From 6c541057239a71379a67cd67efa53b69f0035491 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 08:50:59 +0000 Subject: [PATCH 01/87] feat: add CROSS_ORIGIN_STORAGE support for Wasm loading Adds a new -sCROSS_ORIGIN_STORAGE=1 compiler flag that enables progressive enhancement of Wasm loading via the Cross-Origin Storage browser API, targeting web environments only. Changes: - src/settings.js: adds CROSS_ORIGIN_STORAGE flag (default 0/off) - src/settings_internal.js: adds WASM_SHA256 internal variable - tools/link.py: computes SHA-256 of final .wasm after wasm-opt and injects it into settings as WASM_SHA256, available to JS templates - src/preamble.js: in instantiateAsync(), attempts COS cache lookup by hash before falling back to the normal streaming fetch path; on a cache miss, stores the fetched bytes in COS for reuse The COS path is guarded by #if ENVIRONMENT_MAY_BE_WEB and or when the feature is not enabled. --- src/preamble.js | 31 +++++++++++++++++++++++++++++++ src/settings.js | 14 ++++++++++++++ src/settings_internal.js | 5 +++++ tools/link.py | 15 +++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/src/preamble.js b/src/preamble.js index 97f6366910344..21fe845fc3d87 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -626,6 +626,37 @@ async function instantiateArrayBuffer(binaryFile, imports) { async function instantiateAsync(binary, binaryFile, imports) { #if !SINGLE_FILE +#if ENVIRONMENT_MAY_BE_WEB +#if CROSS_ORIGIN_STORAGE + // Cross-Origin Storage (COS) progressive enhancement. + // The SHA-256 hash of the .wasm binary is computed at link time and embedded + // here. At runtime we check whether the browser exposes the COS API; if so + // we attempt to retrieve the already-compiled module from the shared + // cross-origin cache keyed by its hash, avoiding a network round-trip. + // On a cache miss we fall through to the normal fetch path and then store the + // fetched bytes back into COS for other origins to reuse in the future. + var wasmHash = '{{{ WASM_SHA256 }}}'; + if (wasmHash && typeof crossOriginStorage !== 'undefined' && crossOriginStorage) { + try { + var cosResult = await crossOriginStorage.get(wasmHash); + if (cosResult) { + // Cache hit — instantiate directly from the stored ArrayBuffer. + return WebAssembly.instantiate(cosResult, imports); + } + // Cache miss — fetch over the network, store in COS, then instantiate. + var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); + var wasmBytes = await networkResponse.arrayBuffer(); + // Store asynchronously; don't block instantiation on the write. + crossOriginStorage.set(wasmHash, wasmBytes).catch((e) => err(`COS store failed: ${e}`)); + return WebAssembly.instantiate(wasmBytes, imports); + } catch (cosReason) { + // COS look-up failed for an unexpected reason; fall through to the + // standard streaming path below so the page still loads. + err(`Cross-Origin Storage lookup failed: ${cosReason}`); + } + } +#endif // CROSS_ORIGIN_STORAGE +#endif // ENVIRONMENT_MAY_BE_WEB if (!binary #if MIN_SAFARI_VERSION < 150000 // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming diff --git a/src/settings.js b/src/settings.js index 1f280f49d91af..8c8490be66145 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2205,6 +2205,20 @@ var GROWABLE_ARRAYBUFFERS = false; // indirectly using `importScripts` var CROSS_ORIGIN = false; +// [experimental] Enables Cross-Origin Storage (COS) API support for Wasm +// loading on the Web target. When enabled, Emscripten will compute the +// SHA-256 hash of the final .wasm binary at link time, embed it in the +// generated JS glue, and use the browser COS API (if available) as a +// progressive enhancement: the runtime first tries to retrieve the Wasm +// module from the shared cross-origin cache keyed by its hash. On a cache +// miss the module is fetched over the network as usual and then stored in +// COS so that other origins can reuse it. Falls back transparently to the +// standard fetch path when the browser does not expose the COS API. +// Only meaningful for the Web environment; has no effect elsewhere. +// Usage: emcc -sCROSS_ORIGIN_STORAGE=1 ... +// [link] +var CROSS_ORIGIN_STORAGE = 0; + // This setting changes the behaviour of the ``-shared`` flag. When set to true // you get the old emscripten behaviour where the ``-shared`` flag actually // produces a normal object file (i.e. ``ld -r``). When set to true (the diff --git a/src/settings_internal.js b/src/settings_internal.js index ef72ff8a4789c..482943d54f7bf 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -127,6 +127,11 @@ var USER_EXPORTS = []; // name of the file containing wasm binary, if relevant var WASM_BINARY_FILE = ''; +// SHA-256 hex digest of the final .wasm binary, computed at link time. +// Populated automatically by tools/link.py when CROSS_ORIGIN_STORAGE=1. +// Available in JS glue templates as {{{ WASM_SHA256 }}}. +var WASM_SHA256 = ''; + // Base URL the source mapfile, if relevant var SOURCE_MAP_BASE = ''; diff --git a/tools/link.py b/tools/link.py index 241cbef628a11..b2308ef67948c 100644 --- a/tools/link.py +++ b/tools/link.py @@ -5,6 +5,7 @@ import base64 import glob +import hashlib import json import logging import os @@ -1901,6 +1902,20 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat phase_binaryen(target, options, wasm_target) + # Compute the SHA-256 of the final .wasm binary and make it available to the + # JS glue preprocessor as {{{ WASM_SHA256 }}}. We do this after phase_binaryen + # so that wasm-opt transformations are already reflected in the hash. + if settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB: + if os.path.exists(wasm_target): + with open(wasm_target, 'rb') as f: + wasm_bytes = f.read() + settings.WASM_SHA256 = hashlib.sha256(wasm_bytes).hexdigest() + logger.debug(f'CROSS_ORIGIN_STORAGE: wasm SHA-256 = {settings.WASM_SHA256}') + else: + # Inline / SINGLE_FILE builds embed the wasm in JS; no file to hash. + logger.warning('CROSS_ORIGIN_STORAGE: wasm file not found for hashing; WASM_SHA256 will be empty') + settings.WASM_SHA256 = '' + # If we are not emitting any JS then we are all done now if options.oformat != OFormat.WASM: phase_final_emitting(options, target, js_target, wasm_target) From fcb5526525f65370ef03773671d8f0850b78188e Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 08:57:06 +0000 Subject: [PATCH 02/87] fix: correct COS implementation to match WICG explainer API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation used a fabricated API surface. This commit aligns with the actual spec at https://github.com/WICG/cross-origin-storage Key corrections: - Feature detection: 'crossOriginStorage' in navigator (not typeof crossOriginStorage !== 'undefined') - Hash format: { algorithm: 'SHA-256', value: '' } (not a bare string; spec requires this exact object shape and throws TypeError if value is not a valid lowercase hex string of length 64) - Cache-hit read: requestFileHandles([hash]) → handle.getFile() → blob.arrayBuffer() → WebAssembly.instantiate() (not a .get() method that returns an ArrayBuffer directly) - Cache-miss write: requestFileHandles([hash], { create: true, origins: '*' }) → handle.createWritable() → writable.write(new Blob([bytes])) → writable.close() - origins:'*' makes the Wasm module globally available, which is appropriate for public Wasm assets used across many sites - the UA verifies the hash against the written Blob, so we must write a Blob (not a raw ArrayBuffer) - store is fire-and-forget (async IIFE) so instantiation is never blocked on the write completing - Error handling: distinguish NotFoundError (cache miss, recoverable) from NotAllowedError / other errors (fall through to standard path) --- src/preamble.js | 76 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/src/preamble.js b/src/preamble.js index 21fe845fc3d87..0e02cc21b02ee 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -629,30 +629,62 @@ async function instantiateAsync(binary, binaryFile, imports) { #if ENVIRONMENT_MAY_BE_WEB #if CROSS_ORIGIN_STORAGE // Cross-Origin Storage (COS) progressive enhancement. - // The SHA-256 hash of the .wasm binary is computed at link time and embedded - // here. At runtime we check whether the browser exposes the COS API; if so - // we attempt to retrieve the already-compiled module from the shared - // cross-origin cache keyed by its hash, avoiding a network round-trip. - // On a cache miss we fall through to the normal fetch path and then store the - // fetched bytes back into COS for other origins to reuse in the future. - var wasmHash = '{{{ WASM_SHA256 }}}'; - if (wasmHash && typeof crossOriginStorage !== 'undefined' && crossOriginStorage) { + // https://github.com/WICG/cross-origin-storage + // + // The SHA-256 hash of the final .wasm binary is computed at link time and + // embedded here as a build-time constant. At runtime we feature-detect the + // browser COS API via `'crossOriginStorage' in navigator`, then call + // navigator.crossOriginStorage.requestFileHandles() with the hash object + // required by the spec ({ algorithm: 'SHA-256', value: '' }). + // + // Cache-hit path: + // requestFileHandles() succeeds → getFile() → arrayBuffer() → instantiate + // + // Cache-miss path (NotFoundError): + // fetch() the wasm over the network → instantiate → store in COS with + // origins:'*' (appropriate for a public Wasm module that any site may + // benefit from). The store is fire-and-forget so it never delays startup. + // + // Any other error (NotAllowedError, network failure, …) falls through to the + // standard Emscripten streaming path so the page always loads. + var wasmHashValue = '{{{ WASM_SHA256 }}}'; + if (wasmHashValue && 'crossOriginStorage' in navigator) { + var cosHash = { algorithm: 'SHA-256', value: wasmHashValue }; try { - var cosResult = await crossOriginStorage.get(wasmHash); - if (cosResult) { - // Cache hit — instantiate directly from the stored ArrayBuffer. - return WebAssembly.instantiate(cosResult, imports); + var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); + // Cache hit — read the Blob and instantiate from its ArrayBuffer. + var cosFile = await cosHandles[0].getFile(); + var cosBytes = await cosFile.arrayBuffer(); + return WebAssembly.instantiate(cosBytes, imports); + } catch (cosErr) { + if (cosErr.name === 'NotFoundError') { + // Cache miss — fetch normally, then store in COS for future consumers. + try { + var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); + var wasmBytes = await networkResponse.arrayBuffer(); + // Fire-and-forget store; never block instantiation on the write. + (async () => { + try { + var writeHandles = await navigator.crossOriginStorage.requestFileHandles( + [cosHash], + { create: true, origins: '*' }, + ); + var writable = await writeHandles[0].createWritable(); + await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); + await writable.close(); + } catch (storeErr) { + err(`COS store failed: ${storeErr}`); + } + })(); + return WebAssembly.instantiate(wasmBytes, imports); + } catch (fetchErr) { + // Network fetch failed; fall through to the standard path. + err(`COS fallback fetch failed: ${fetchErr}`); + } + } else { + // NotAllowedError or unexpected error; fall through gracefully. + err(`Cross-Origin Storage lookup failed: ${cosErr}`); } - // Cache miss — fetch over the network, store in COS, then instantiate. - var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); - var wasmBytes = await networkResponse.arrayBuffer(); - // Store asynchronously; don't block instantiation on the write. - crossOriginStorage.set(wasmHash, wasmBytes).catch((e) => err(`COS store failed: ${e}`)); - return WebAssembly.instantiate(wasmBytes, imports); - } catch (cosReason) { - // COS look-up failed for an unexpected reason; fall through to the - // standard streaming path below so the page still loads. - err(`Cross-Origin Storage lookup failed: ${cosReason}`); } } #endif // CROSS_ORIGIN_STORAGE From 6ed1b0b57a9fe797025804f9c97157cb28e3b639 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:07:44 +0000 Subject: [PATCH 03/87] test: add test suite for CROSS_ORIGIN_STORAGE feature Five tests in test/test_other.py covering all meaningful static build output properties of the feature (no browser runtime required): test_cross_origin_storage_js_output - Verifies all expected API surface is present in the emitted JS when -sCROSS_ORIGIN_STORAGE=1 -sENVIRONMENT=web is used: 'crossOriginStorage' in navigator (feature detection) navigator.crossOriginStorage.requestFileHandles (correct API) algorithm: 'SHA-256' / value: '' (spec hash object shape) origins: '*' (globally-available flag for public Wasm) getFile() / createWritable() (read and write paths) 'NotFoundError' / 'NotAllowedError' (error discrimination) - Reads the emitted .wasm file and verifies the embedded hash value exactly matches hashlib.sha256(wasm_bytes).hexdigest() test_cross_origin_storage_disabled_by_default - Verifies crossOriginStorage is absent from JS output when the flag is not passed (default CROSS_ORIGIN_STORAGE=0) test_cross_origin_storage_not_emitted_for_node_target - Verifies the #if ENVIRONMENT_MAY_BE_WEB preprocessor guard strips all COS code when -sENVIRONMENT=node, even with the flag enabled test_cross_origin_storage_not_emitted_for_single_file - Verifies COS code is absent in SINGLE_FILE builds where there is no standalone .wasm file to hash test_cross_origin_storage_hash_changes_with_content - Compiles two different source files and asserts their embedded hashes differ, confirming the hash reflects actual binary content --- test/test_other.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/test/test_other.py b/test/test_other.py index 4e31228aea2b0..f16fb0c349fa5 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -5,6 +5,7 @@ import glob +import hashlib import importlib import itertools import json @@ -15575,3 +15576,97 @@ def test_deprecated_settings(self): err = self.run_process([EMCC, '-sUSE_PTHREADS', test_file('hello_world.c')], stderr=PIPE).stderr self.assertContained('emcc: warning: USE_PTHREADS is deprecated (prefer the standard -pthread flag). Please open a bug if you have a continuing need for this setting [-Wdeprecated]', err) + + + # --------------------------------------------------------------------------- + # Tests for CROSS_ORIGIN_STORAGE (-sCROSS_ORIGIN_STORAGE=1) + # https://github.com/WICG/cross-origin-storage + # --------------------------------------------------------------------------- + + def test_cross_origin_storage_js_output(self): + """COS code is present in JS when the feature is enabled for the web target, + and the embedded hash is the correct SHA-256 of the compiled .wasm file.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js = read_file('hello.js') + + # Feature-detect pattern from the WICG explainer. + self.assertContained("'crossOriginStorage' in navigator", js) + + # Correct API call. + self.assertContained('navigator.crossOriginStorage.requestFileHandles', js) + + # Hash object shape required by the spec. + self.assertContained("algorithm: 'SHA-256'", js) + + # Globally-available flag appropriate for a public Wasm module. + self.assertContained("origins: '*'", js) + + # Cache-hit and cache-miss path markers. + self.assertContained('getFile()', js) + self.assertContained('createWritable()', js) + + # Error name discrimination. + self.assertContained("'NotFoundError'", js) + self.assertContained("'NotAllowedError'", js) + + # The hash embedded in the JS must be a 64-char lowercase hex string … + m = re.search(r"value:\s*'([0-9a-f]{64})'", js) + self.assertTrue(m, 'could not find a 64-char hex WASM_SHA256 value in JS output') + embedded_hash = m.group(1) + + # … and must exactly match the SHA-256 of the emitted .wasm file. + expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() + self.assertEqual(embedded_hash, expected_hash, + 'embedded WASM_SHA256 does not match actual .wasm SHA-256') + + def test_cross_origin_storage_disabled_by_default(self): + """COS code must NOT appear when the flag is omitted (default off).""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertNotContained('crossOriginStorage', js) + + def test_cross_origin_storage_not_emitted_for_node_target(self): + """COS code must NOT appear when targeting Node.js only, even with the flag + set; the #if ENVIRONMENT_MAY_BE_WEB guard should strip it.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=node', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertNotContained('crossOriginStorage', js) + + def test_cross_origin_storage_not_emitted_for_single_file(self): + """COS code must NOT appear in SINGLE_FILE builds (wasm is inlined as + base64; there is no standalone .wasm file to key the hash on).""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sSINGLE_FILE', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertNotContained('crossOriginStorage', js) + + def test_cross_origin_storage_hash_changes_with_content(self): + """Two different programs must produce different embedded hashes.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js_a = read_file('hello.js') + hash_a = re.search(r"value:\s*'([0-9a-f]{64})'", js_a).group(1) + + self.run_process([EMCC, test_file('hello_world_small.c'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-o', 'small.js']) + js_b = read_file('small.js') + hash_b = re.search(r"value:\s*'([0-9a-f]{64})'", js_b).group(1) + + self.assertNotEqual(hash_a, hash_b, + 'different programs should produce different WASM_SHA256 hashes') + From e95e99ff5d5d5a3be8f1f75bef0a41a4bcf27679 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:12:05 +0000 Subject: [PATCH 04/87] docs: add Cross-Origin Storage guide and polish setting comment - site/source/docs/compiling/CrossOriginStorage.rst (new) Full guide covering: overview, usage, build-time / runtime behaviour, testing with the extension polyfill, hash verification, and relationship to other caching mechanisms. Links to the WICG explainer and extension. - site/source/docs/compiling/index.rst Register CrossOriginStorage in the section toctree and bullet list. - src/settings.js Expand the CROSS_ORIGIN_STORAGE comment into RST-friendly prose that the auto-generated settings_reference.rst page will render cleanly, including a cross-reference to the new guide page. --- .../docs/compiling/CrossOriginStorage.rst | 150 ++++++++++++++++++ site/source/docs/compiling/index.rst | 2 + src/settings.js | 33 ++-- 3 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 site/source/docs/compiling/CrossOriginStorage.rst diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst new file mode 100644 index 0000000000000..04cf2a89d31a0 --- /dev/null +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -0,0 +1,150 @@ +.. _CrossOriginStorage: + +========================== +Cross-Origin Storage (COS) +========================== + +.. note:: + This feature is **experimental**. The underlying `Cross-Origin Storage + browser API `_ is a WICG + proposal that has not yet shipped in any browser. Emscripten's support is + provided as a progressive enhancement — the runtime falls back to the + standard ``fetch()`` path automatically when the browser does not expose + the API. + +Overview +======== + +The **Cross-Origin Storage (COS)** API is a proposed browser standard that +allows web applications on different origins to share large cached files, +identified by their cryptographic hashes. A file stored in COS by one site +can be retrieved by any other site using the same hash, eliminating redundant +downloads. + +Emscripten's ``-sCROSS_ORIGIN_STORAGE=1`` flag integrates this into the +standard Wasm loading path. At build time, Emscripten computes the SHA-256 +hash of the final ``.wasm`` binary. At runtime, the generated JavaScript +tries to retrieve the compiled Wasm module from COS before falling back to +a normal network fetch. If the module is not yet in COS it is stored there +after download, making it available to other origins immediately. + +This is particularly beneficial for popular Wasm modules that many sites +ship independently — game engines, scientific computing runtimes, and +frameworks such as Flutter or Pyodide — where users would otherwise download +the same bytes many times. + +Usage +===== + +Pass ``-sCROSS_ORIGIN_STORAGE=1`` at link time:: + + emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 + +The flag is a **link-time** setting and has no effect during compilation of +individual object files. + +Requirements and restrictions +------------------------------ + +- The flag only has an effect when the output targets the **web** environment. + It is silently ignored for Node.js-only or shell targets (``-sENVIRONMENT=node``). +- It has no effect in **SINGLE_FILE** mode (``-sSINGLE_FILE``), because the + Wasm binary is embedded inline as base64 and there is no standalone + ``.wasm`` file to hash. +- The COS API is a progressive enhancement. Browsers without the API + continue to load the Wasm module via the normal ``fetch()`` and + ``WebAssembly.instantiateStreaming()`` path without any error. + +How it works +============ + +Build time +---------- + +After all optimizations — including any ``wasm-opt`` passes run by Binaryen +— Emscripten reads the final ``.wasm`` binary and computes its SHA-256 +digest. That digest is embedded in the generated JavaScript glue as a +build-time constant:: + + var wasmHashValue = 'a3f2...c9d1'; // 64 hex characters + +No extra files are produced; the hash is part of the regular ``.js`` output. + +Runtime (web only) +------------------ + +When the page loads, the generated JavaScript follows this logic: + +1. **Feature detection** — check ``'crossOriginStorage' in navigator``. + If the API is absent, skip to the normal fetch path immediately. + +2. **Cache hit** — call + ``navigator.crossOriginStorage.requestFileHandles([{algorithm: 'SHA-256', value: wasmHashValue}])``. + If the handle is returned (the module is already in COS), read it with + ``handle.getFile()`` → ``.arrayBuffer()`` and pass the bytes to + ``WebAssembly.instantiate()``. + +3. **Cache miss** — if a ``NotFoundError`` is thrown, fetch the ``.wasm`` + over the network as usual, call ``WebAssembly.instantiate()`` immediately + so the page loads without delay, and then write the bytes into COS in the + background (fire-and-forget) with ``origins: '*'`` so any other origin + can benefit:: + + navigator.crossOriginStorage.requestFileHandles([hash], { create: true, origins: '*' }) + +4. **Fallback** — any unexpected error (``NotAllowedError`` from the browser, + network failure during the miss path, etc.) is logged with ``err()`` and + the runtime falls through to the standard streaming-instantiation path + below. The page always loads. + +Testing with the extension polyfill +==================================== + +Because no browser ships the COS API natively yet, you can experiment using +the `Cross-Origin Storage extension +`_, +which injects a ``navigator.crossOriginStorage`` polyfill on every page. + +1. Install the extension in Chrome. +2. Build your project with ``-sCROSS_ORIGIN_STORAGE=1 -sENVIRONMENT=web``. +3. Serve the output over HTTP (e.g. with ``emrun`` or ``python3 -m http.server``). +4. Open the page — on the first load the Wasm binary is fetched and stored in + COS. Open the same page in a second tab or from a different origin: the + module is loaded from COS without a network request. + +Verifying the embedded hash +============================ + +You can confirm that the hash embedded in the ``.js`` output matches the +actual ``.wasm`` file using standard tools: + +.. code-block:: bash + + # SHA-256 of the wasm file + sha256sum hello.wasm + + # Extract the hash embedded in the JS + grep -oP "value: '\K[0-9a-f]{64}" hello.js + +Both values must be identical. The Emscripten test suite checks this +automatically via ``test_cross_origin_storage_js_output`` in +``test/test_other.py``. + +Relationship to other caching mechanisms +========================================== + +COS is a complement to, not a replacement for, existing browser caches: + +- **HTTP cache / Service Worker cache** — still used for per-origin caching. + COS adds cross-origin sharing on top. +- **``NODE_CODE_CACHING``** — a Node.js-specific V8 bytecode cache; unrelated + to COS. +- **IndexedDB / OPFS** — per-origin storage; COS shares across origins. + +See also +======== + +- `WICG Cross-Origin Storage explainer `_ +- `COS browser extension (polyfill) `_ +- :ref:`settings-reference` — ``CROSS_ORIGIN_STORAGE`` entry +- :ref:`WebAssembly` — general guide to building Wasm with Emscripten diff --git a/site/source/docs/compiling/index.rst b/site/source/docs/compiling/index.rst index e8b03becc832d..6aa22ae9c5a70 100644 --- a/site/source/docs/compiling/index.rst +++ b/site/source/docs/compiling/index.rst @@ -13,6 +13,7 @@ This section contains topics about building projects and running the output. - :ref:`Deploying-Pages` covers topics related to hosting Emscripten compiled web pages on a CDN. - :ref:`GitLab` explains how to build and test projects on GitLab. - :ref:`Contrib-Ports` contains information about contrib ports. +- :ref:`CrossOriginStorage` explains how to enable the experimental Cross-Origin Storage integration for sharing Wasm modules across origins. .. toctree:: @@ -26,3 +27,4 @@ This section contains topics about building projects and running the output. Deploying-Pages GitLab Contrib-Ports + CrossOriginStorage diff --git a/src/settings.js b/src/settings.js index 8c8490be66145..e1b80dab4b301 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2206,16 +2206,29 @@ var GROWABLE_ARRAYBUFFERS = false; var CROSS_ORIGIN = false; // [experimental] Enables Cross-Origin Storage (COS) API support for Wasm -// loading on the Web target. When enabled, Emscripten will compute the -// SHA-256 hash of the final .wasm binary at link time, embed it in the -// generated JS glue, and use the browser COS API (if available) as a -// progressive enhancement: the runtime first tries to retrieve the Wasm -// module from the shared cross-origin cache keyed by its hash. On a cache -// miss the module is fetched over the network as usual and then stored in -// COS so that other origins can reuse it. Falls back transparently to the -// standard fetch path when the browser does not expose the COS API. -// Only meaningful for the Web environment; has no effect elsewhere. -// Usage: emcc -sCROSS_ORIGIN_STORAGE=1 ... +// loading on the Web target. +// +// When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` +// binary at link time, embeds it as a build-time constant in the generated +// JavaScript glue, and uses the browser COS API as a progressive enhancement: +// +// - **Cache hit**: the runtime calls +// ``navigator.crossOriginStorage.requestFileHandles()`` with the hash and, +// if the module is found, reads it directly from the cross-origin cache. +// - **Cache miss**: the module is fetched over the network as usual, then +// stored in COS in the background (non-blocking) with ``origins: '*'`` so +// any other origin can reuse it. +// - **Fallback**: when the browser does not expose the COS API, or when an +// unexpected error occurs, the runtime falls through to the standard +// ``fetch`` / ``WebAssembly.instantiateStreaming`` path transparently. +// +// Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no +// effect on Node.js or shell targets. Also has no effect in SINGLE_FILE +// builds where the Wasm binary is inlined as base64. +// +// See :ref:`CrossOriginStorage` for the full guide, including how to test +// with the COS browser extension polyfill. +// // [link] var CROSS_ORIGIN_STORAGE = 0; From 0ead00feb9ccce137b62a9a4e06a620b9c42ad09 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:14:16 +0000 Subject: [PATCH 05/87] test: add cross_origin_storage browser example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A self-contained example in test/cross_origin_storage/ demonstrating the -sCROSS_ORIGIN_STORAGE=1 flag: main.cpp — minimal C++ source exporting a greet() function index.html — browser shell that reports COS API availability, hooks Module.print/printErr onto the page, and calls greet() after the runtime initialises README.md — build instructions, prerequisites (COS extension), run instructions, and hash-verification one-liner The example follows the same pattern as test/vite/ and test/webpack/: a directory you build and serve locally to exercise a browser-only feature. --- test/cross_origin_storage/README.md | 66 +++++++++++++++++++++ test/cross_origin_storage/index.html | 88 ++++++++++++++++++++++++++++ test/cross_origin_storage/main.cpp | 30 ++++++++++ 3 files changed, 184 insertions(+) create mode 100644 test/cross_origin_storage/README.md create mode 100644 test/cross_origin_storage/index.html create mode 100644 test/cross_origin_storage/main.cpp diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md new file mode 100644 index 0000000000000..8bef98c5a1cf5 --- /dev/null +++ b/test/cross_origin_storage/README.md @@ -0,0 +1,66 @@ +# Cross-Origin Storage (COS) example + +This example demonstrates Emscripten's experimental +`-sCROSS_ORIGIN_STORAGE=1` flag, which integrates the +[Cross-Origin Storage browser API](https://github.com/WICG/cross-origin-storage) +into the standard Wasm loading path. + +## What it does + +On the **first load** the `.wasm` module is fetched over the network and +stored in the cross-origin cache, keyed by its SHA-256 hash. + +On **subsequent loads** — from the same origin or any other — the module is +retrieved from the cache without a network request for the binary. + +## Prerequisites + +The COS API is not yet natively supported by any browser. Install the +polyfill extension to try it today: + +- **Chrome Web Store**: + [Cross-Origin Storage](https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih) + +## Build + +```bash +emcc main.cpp -o index.js \ + -sCROSS_ORIGIN_STORAGE=1 \ + -sENVIRONMENT=web \ + -sEXPORTED_RUNTIME_METHODS=ccall \ + -sEXPORTED_FUNCTIONS=_greet \ + -sALLOW_MEMORY_GROWTH +``` + +This produces `index.js` and `index.wasm`. The SHA-256 hash of `index.wasm` +is embedded in `index.js` at build time — you can verify them match: + +```bash +sha256sum index.wasm +grep -oP "value: '\K[0-9a-f]{64}" index.js +``` + +## Run + +Serve the directory over HTTP (the `file://` protocol does not support +`fetch()`): + +```bash +emrun . +# or +python3 -m http.server +``` + +Open `http://localhost:8080` (or whichever port your server uses) in a +browser with the COS extension installed. + +Open DevTools → Console. On the first load you should see a cache-miss log +message and a network request for `index.wasm` in the Network tab. Reload +the page — the network request disappears and the console shows that the +module was served from the cross-origin cache. + +## See also + +- [COS Emscripten docs](../../site/source/docs/compiling/CrossOriginStorage.rst) +- [WICG explainer](https://github.com/WICG/cross-origin-storage) +- [COS extension source](https://github.com/web-ai-community/cross-origin-storage-extension) diff --git a/test/cross_origin_storage/index.html b/test/cross_origin_storage/index.html new file mode 100644 index 0000000000000..fb0e82f17ab7d --- /dev/null +++ b/test/cross_origin_storage/index.html @@ -0,0 +1,88 @@ + + + + + + Emscripten — Cross-Origin Storage example + + + +

Emscripten — Cross-Origin Storage example

+

+ Open the browser DevTools console to see detailed COS timing. + Reload the page after the first load to observe the cache-hit path. +

+
Loading…
+ + + + + + + diff --git a/test/cross_origin_storage/main.cpp b/test/cross_origin_storage/main.cpp new file mode 100644 index 0000000000000..dd33fce450ac6 --- /dev/null +++ b/test/cross_origin_storage/main.cpp @@ -0,0 +1,30 @@ +// Copyright 2025 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. + +// Example: Cross-Origin Storage (COS) integration +// +// Build with: +// emcc main.cpp -o index.js \ +// -sCROSS_ORIGIN_STORAGE=1 \ +// -sENVIRONMENT=web \ +// -sEXPORTED_RUNTIME_METHODS=ccall \ +// -sEXPORTED_FUNCTIONS=_greet \ +// -sALLOW_MEMORY_GROWTH +// +// Serve the directory over HTTP (e.g. `emrun .` or `python3 -m http.server`) +// and open index.html in a browser that has the COS extension installed. + +#include +#include + +// Called from JavaScript after the module loads. +extern "C" { + +EMSCRIPTEN_KEEPALIVE +const char* greet() { + return "Hello from WebAssembly!"; +} + +} // extern "C" From 3b49dc4709491f0aa27833716457ddd436da602b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:19:22 +0000 Subject: [PATCH 06/87] feat: add COS instrumentation callbacks and update example/docs/tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/preamble.js Add three optional Module callbacks invoked at each COS runtime event: - Module['onCOSCacheHit'](hash) — Wasm served from cross-origin cache - Module['onCOSCacheMiss'](url) — Wasm not in COS, fetched from network - Module['onCOSStore'](hash) — Wasm successfully written to COS test/cross_origin_storage/index.html Rewrite to use the new callbacks exclusively. No mention of any particular implementation. Reports: - whether the COS API is active or not - on miss: the network URL the Wasm was fetched from, then the hash once it has been stored - on hit: the SHA-256 hash of the Wasm resource served from COS test/cross_origin_storage/README.md Remove extension references; describe only the observable behaviour (which path was taken, hash, URL). site/source/docs/compiling/CrossOriginStorage.rst Document the three callbacks with a usage example, and fold them into the runtime flow description. test/test_other.py Assert all three Module callback strings are present in the emitted JS when CROSS_ORIGIN_STORAGE=1. --- .../docs/compiling/CrossOriginStorage.rst | 35 ++++++++++++-- src/preamble.js | 9 ++++ test/cross_origin_storage/README.md | 24 ++++------ test/cross_origin_storage/index.html | 47 ++++++++++--------- test/test_other.py | 5 ++ 5 files changed, 78 insertions(+), 42 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 04cf2a89d31a0..69206471a71d4 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -83,12 +83,14 @@ When the page loads, the generated JavaScript follows this logic: If the handle is returned (the module is already in COS), read it with ``handle.getFile()`` → ``.arrayBuffer()`` and pass the bytes to ``WebAssembly.instantiate()``. + Then invoke ``Module['onCOSCacheHit'](hash)`` if defined. 3. **Cache miss** — if a ``NotFoundError`` is thrown, fetch the ``.wasm`` - over the network as usual, call ``WebAssembly.instantiate()`` immediately - so the page loads without delay, and then write the bytes into COS in the - background (fire-and-forget) with ``origins: '*'`` so any other origin - can benefit:: + over the network as usual, invoke ``Module['onCOSCacheMiss'](url)`` if + defined, call ``WebAssembly.instantiate()`` immediately so the page loads + without delay, and then write the bytes into COS in the background + (fire-and-forget) with ``origins: '*'`` so any other origin can benefit. + Once the write completes, invoke ``Module['onCOSStore'](hash)`` if defined:: navigator.crossOriginStorage.requestFileHandles([hash], { create: true, origins: '*' }) @@ -97,6 +99,31 @@ When the page loads, the generated JavaScript follows this logic: the runtime falls through to the standard streaming-instantiation path below. The page always loads. +Instrumentation callbacks +------------------------- + +Three optional ``Module`` properties let you observe COS events at runtime: + +.. code-block:: javascript + + var Module = { + // Called when the Wasm binary was served from the cross-origin cache. + onCOSCacheHit: (hash) => { + console.log('Cache hit, SHA-256:', hash); + }, + + // Called when the Wasm binary was not in COS and was fetched over the + // network. |url| is the resolved URL of the .wasm file. + onCOSCacheMiss: (url) => { + console.log('Cache miss, fetched from:', url); + }, + + // Called after the Wasm binary has been successfully written to COS. + onCOSStore: (hash) => { + console.log('Stored in COS, SHA-256:', hash); + }, + }; + Testing with the extension polyfill ==================================== diff --git a/src/preamble.js b/src/preamble.js index 0e02cc21b02ee..6d78d19f0b249 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -655,6 +655,9 @@ async function instantiateAsync(binary, binaryFile, imports) { // Cache hit — read the Blob and instantiate from its ArrayBuffer. var cosFile = await cosHandles[0].getFile(); var cosBytes = await cosFile.arrayBuffer(); + // Optional instrumentation callback: Module['onCOSCacheHit'](hash) + // Called when the Wasm binary is served from the cross-origin cache. + if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](wasmHashValue); return WebAssembly.instantiate(cosBytes, imports); } catch (cosErr) { if (cosErr.name === 'NotFoundError') { @@ -662,6 +665,9 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); var wasmBytes = await networkResponse.arrayBuffer(); + // Optional instrumentation callback: Module['onCOSCacheMiss'](url) + // Called when the Wasm binary is not in COS and is fetched over the network. + if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](binaryFile); // Fire-and-forget store; never block instantiation on the write. (async () => { try { @@ -672,6 +678,9 @@ async function instantiateAsync(binary, binaryFile, imports) { var writable = await writeHandles[0].createWritable(); await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); await writable.close(); + // Optional instrumentation callback: Module['onCOSStore'](hash) + // Called after the Wasm binary has been successfully written to COS. + if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](wasmHashValue); } catch (storeErr) { err(`COS store failed: ${storeErr}`); } diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md index 8bef98c5a1cf5..7e347edd8e434 100644 --- a/test/cross_origin_storage/README.md +++ b/test/cross_origin_storage/README.md @@ -13,13 +13,8 @@ stored in the cross-origin cache, keyed by its SHA-256 hash. On **subsequent loads** — from the same origin or any other — the module is retrieved from the cache without a network request for the binary. -## Prerequisites - -The COS API is not yet natively supported by any browser. Install the -polyfill extension to try it today: - -- **Chrome Web Store**: - [Cross-Origin Storage](https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih) +The page reports which path was taken and, where applicable, the SHA-256 hash +of the Wasm resource and the URL it was fetched from. ## Build @@ -33,7 +28,7 @@ emcc main.cpp -o index.js \ ``` This produces `index.js` and `index.wasm`. The SHA-256 hash of `index.wasm` -is embedded in `index.js` at build time — you can verify them match: +is embedded in `index.js` at build time — you can verify they match: ```bash sha256sum index.wasm @@ -51,16 +46,15 @@ emrun . python3 -m http.server ``` -Open `http://localhost:8080` (or whichever port your server uses) in a -browser with the COS extension installed. +Open the page in a browser with the Cross-Origin Storage API available. + +The page will report: -Open DevTools → Console. On the first load you should see a cache-miss log -message and a network request for `index.wasm` in the Network tab. Reload -the page — the network request disappears and the console shows that the -module was served from the cross-origin cache. +- whether the COS API is active +- on a cache miss: the URL the Wasm was fetched from, and confirmation once it has been stored in COS with its hash +- on a cache hit: the SHA-256 hash of the Wasm resource served from COS ## See also - [COS Emscripten docs](../../site/source/docs/compiling/CrossOriginStorage.rst) - [WICG explainer](https://github.com/WICG/cross-origin-storage) -- [COS extension source](https://github.com/web-ai-community/cross-origin-storage-extension) diff --git a/test/cross_origin_storage/index.html b/test/cross_origin_storage/index.html index fb0e82f17ab7d..9b284c699a107 100644 --- a/test/cross_origin_storage/index.html +++ b/test/cross_origin_storage/index.html @@ -2,16 +2,8 @@ @@ -40,10 +32,7 @@

Emscripten — Cross-Origin Storage example

-

- Open the browser DevTools console to see detailed COS timing. - Reload the page after the first load to observe the cache-hit path. -

+

Reload the page after the first load to observe the cache-hit path.

Loading…
diff --git a/test/test_other.py b/test/test_other.py index f16fb0c349fa5..43fe58b9841d5 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15612,6 +15612,11 @@ def test_cross_origin_storage_js_output(self): self.assertContained("'NotFoundError'", js) self.assertContained("'NotAllowedError'", js) + # Instrumentation callbacks invoked at each COS event. + self.assertContained("Module['onCOSCacheHit']", js) + self.assertContained("Module['onCOSCacheMiss']", js) + self.assertContained("Module['onCOSStore']", js) + # The hash embedded in the JS must be a 64-char lowercase hex string … m = re.search(r"value:\s*'([0-9a-f]{64})'", js) self.assertTrue(m, 'could not find a 64-char hex WASM_SHA256 value in JS output') From 57a12b2e6821de7d1a9e24ad6ae5c4597aca7f8f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:22:34 +0000 Subject: [PATCH 07/87] fix: add link-time validation and changelog entry for CROSS_ORIGIN_STORAGE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/link.py - CROSS_ORIGIN_STORAGE + SINGLE_FILE → hard error at link time (wasm is inlined as base64; there is no file to hash) - CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 → warning (sync instantiation bypasses the async COS fetch path entirely) test/test_other.py Replace test_cross_origin_storage_not_emitted_for_single_file with two sharper tests: - test_cross_origin_storage_error_with_single_file: asserts the expected error message via assert_fail - test_cross_origin_storage_warning_without_async_compilation: asserts the warning appears in stderr ChangeLog.md Add entry under 6.0.1 (in development) describing the new flag, its build-time/runtime behaviour, the three Module callbacks, and the two incompatible flag combinations. --- ChangeLog.md | 14 ++++++++++++++ test/test_other.py | 23 ++++++++++++++++------- tools/link.py | 6 ++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 446752d526dc9..48cce273c678c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -35,6 +35,20 @@ See docs/process.md for more on how version tagging works. supported. (#23493) - Fixed `getentropy`/`random_get` spuriously failing under Node.js and the shell environment for small requests. (#27122) +- New experimental ``-sCROSS_ORIGIN_STORAGE=1`` linker flag that integrates + the proposed `Cross-Origin Storage browser API + `_ into the Wasm loading path + as a progressive enhancement (web target only). At build time Emscripten + computes the SHA-256 hash of the final ``.wasm`` binary and embeds it in the + generated JS glue. At runtime, if the browser exposes + ``navigator.crossOriginStorage``, the runtime first attempts to retrieve the + module from the shared cross-origin cache (cache hit); on a miss it fetches + normally and stores the binary in COS for future use by any origin. Falls + back transparently to the standard fetch path when the API is unavailable. + Three optional ``Module`` callbacks are available for instrumentation: + ``Module['onCOSCacheHit'](hash)``, ``Module['onCOSCacheMiss'](url)``, and + ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` + (hard error) and ``-sWASM_ASYNC_COMPILATION=0`` (warning). 6.0.0 - 06/04/26 ---------------- diff --git a/test/test_other.py b/test/test_other.py index 43fe58b9841d5..b24fac228e73c 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15645,16 +15645,25 @@ def test_cross_origin_storage_not_emitted_for_node_target(self): js = read_file('hello.js') self.assertNotContained('crossOriginStorage', js) - def test_cross_origin_storage_not_emitted_for_single_file(self): - """COS code must NOT appear in SINGLE_FILE builds (wasm is inlined as - base64; there is no standalone .wasm file to key the hash on).""" - self.run_process([EMCC, test_file('hello_world.cpp'), + def test_cross_origin_storage_error_with_single_file(self): + """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" + self.assert_fail([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', '-sSINGLE_FILE', - '-o', 'hello.js']) - js = read_file('hello.js') - self.assertNotContained('crossOriginStorage', js) + '-o', 'hello.js'], + 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') + + def test_cross_origin_storage_warning_without_async_compilation(self): + """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must warn.""" + proc = self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sWASM_ASYNC_COMPILATION=0', + '-o', 'hello.js'], + stderr=PIPE) + self.assertContained('CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0', + proc.stderr) def test_cross_origin_storage_hash_changes_with_content(self): """Two different programs must produce different embedded hashes.""" diff --git a/tools/link.py b/tools/link.py index b2308ef67948c..7204f6858fd2e 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1209,6 +1209,12 @@ def limit_incoming_module_api(): if settings.WASM == 2 and settings.SINGLE_FILE: exit_with_error('cannot have both WASM=2 and SINGLE_FILE enabled at the same time') + if settings.CROSS_ORIGIN_STORAGE: + if settings.SINGLE_FILE: + exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined and cannot be identified by hash)') + if not settings.WASM_ASYNC_COMPILATION: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') + if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML: exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output') From c4473a91a8ed14ca24bb7c8b17e9e21fdcc76d8b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:24:23 +0000 Subject: [PATCH 08/87] fix: remove incorrect 'base64' claim in SINGLE_FILE + COS descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SINGLE_FILE uses a custom UTF-8 binary embedding by default (SINGLE_FILE_BINARY_ENCODE=1), not base64. The point that matters is that the wasm is inlined directly into the JS output with no standalone .wasm file or fetchable URL — the encoding method is irrelevant to COS. Updated: settings.js comment, link.py error message, CrossOriginStorage.rst, and ChangeLog.md. --- ChangeLog.md | 3 ++- site/source/docs/compiling/CrossOriginStorage.rst | 7 ++++--- src/settings.js | 2 +- tools/link.py | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 48cce273c678c..748c364d70496 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -48,7 +48,8 @@ See docs/process.md for more on how version tagging works. Three optional ``Module`` callbacks are available for instrumentation: ``Module['onCOSCacheHit'](hash)``, ``Module['onCOSCacheMiss'](url)``, and ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` - (hard error) and ``-sWASM_ASYNC_COMPILATION=0`` (warning). + (hard error — the wasm is inlined directly into the JS output with no + fetchable URL) and ``-sWASM_ASYNC_COMPILATION=0`` (warning). 6.0.0 - 06/04/26 ---------------- diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 69206471a71d4..b6231b398322e 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -48,9 +48,10 @@ Requirements and restrictions - The flag only has an effect when the output targets the **web** environment. It is silently ignored for Node.js-only or shell targets (``-sENVIRONMENT=node``). -- It has no effect in **SINGLE_FILE** mode (``-sSINGLE_FILE``), because the - Wasm binary is embedded inline as base64 and there is no standalone - ``.wasm`` file to hash. +- It produces a **hard link-time error** in **SINGLE_FILE** mode + (``-sSINGLE_FILE``): the Wasm binary is embedded directly into the JS + output and has no standalone ``.wasm`` file or fetchable URL to key the + hash on. - The COS API is a progressive enhancement. Browsers without the API continue to load the Wasm module via the normal ``fetch()`` and ``WebAssembly.instantiateStreaming()`` path without any error. diff --git a/src/settings.js b/src/settings.js index e1b80dab4b301..0fff67409108e 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2224,7 +2224,7 @@ var CROSS_ORIGIN = false; // // Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no // effect on Node.js or shell targets. Also has no effect in SINGLE_FILE -// builds where the Wasm binary is inlined as base64. +// builds where the Wasm binary is inlined directly into the JS output. // // See :ref:`CrossOriginStorage` for the full guide, including how to test // with the COS browser extension polyfill. diff --git a/tools/link.py b/tools/link.py index 7204f6858fd2e..942348d800b8b 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1211,7 +1211,7 @@ def limit_incoming_module_api(): if settings.CROSS_ORIGIN_STORAGE: if settings.SINGLE_FILE: - exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined and cannot be identified by hash)') + exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on)') if not settings.WASM_ASYNC_COMPILATION: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') From 1fbcbc40c31bf1c5b3f65a774697e4e355e75058 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:27:32 +0000 Subject: [PATCH 09/87] feat: warn when CROSS_ORIGIN_STORAGE is used with SPLIT_MODULE or MAIN_MODULE The COS integration only hashes and caches the primary .wasm output. Emscripten can produce additional Wasm files that are outside its scope: - SPLIT_MODULE: produces .deferred.wasm / ..wasm secondary files that are fetched lazily when a deferred function is first called. - MAIN_MODULE: side modules loaded at runtime via dlopen are separate .wasm files fetched through the normal network path. Both combinations now emit a link-time warning explaining that only the primary .wasm is covered, so developers are not surprised when secondary files are always fetched from the network. Tests added for both warning cases in test/test_other.py. CrossOriginStorage.rst updated to document all four limitations (SINGLE_FILE, WASM_ASYNC_COMPILATION=0, SPLIT_MODULE, MAIN_MODULE). --- .../docs/compiling/CrossOriginStorage.rst | 8 +++++++ test/test_other.py | 22 +++++++++++++++++++ tools/link.py | 4 ++++ 3 files changed, 34 insertions(+) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index b6231b398322e..ce3e298159ed1 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -52,6 +52,14 @@ Requirements and restrictions (``-sSINGLE_FILE``): the Wasm binary is embedded directly into the JS output and has no standalone ``.wasm`` file or fetchable URL to key the hash on. +- It emits a **warning** with ``-sWASM_ASYNC_COMPILATION=0``: the + synchronous instantiation path bypasses ``instantiateAsync()`` entirely, + so the COS code is never reached. +- It covers **only the primary ``.wasm`` file**. Secondary files produced by + ``-sSPLIT_MODULE`` (``.deferred.wasm``) and side modules loaded at runtime + via ``dlopen`` in ``-sMAIN_MODULE`` builds are fetched through the normal + network path and are not stored in or retrieved from COS. A warning is + emitted for both of these combinations. - The COS API is a progressive enhancement. Browsers without the API continue to load the Wasm module via the normal ``fetch()`` and ``WebAssembly.instantiateStreaming()`` path without any error. diff --git a/test/test_other.py b/test/test_other.py index b24fac228e73c..272dab95db401 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15665,6 +15665,28 @@ def test_cross_origin_storage_warning_without_async_compilation(self): self.assertContained('CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0', proc.stderr) + def test_cross_origin_storage_warning_with_split_module(self): + """CROSS_ORIGIN_STORAGE + SPLIT_MODULE must warn that secondary files are not covered.""" + proc = self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sSPLIT_MODULE', + '-o', 'hello.js'], + stderr=PIPE) + self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', + proc.stderr) + + def test_cross_origin_storage_warning_with_main_module(self): + """CROSS_ORIGIN_STORAGE + MAIN_MODULE must warn that side modules are not covered.""" + proc = self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sMAIN_MODULE', + '-o', 'hello.js'], + stderr=PIPE) + self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', + proc.stderr) + def test_cross_origin_storage_hash_changes_with_content(self): """Two different programs must produce different embedded hashes.""" self.run_process([EMCC, test_file('hello_world.cpp'), diff --git a/tools/link.py b/tools/link.py index 942348d800b8b..9487cc2f6e1e5 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1214,6 +1214,10 @@ def limit_incoming_module_api(): exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on)') if not settings.WASM_ASYNC_COMPILATION: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') + if settings.SPLIT_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') + if settings.MAIN_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML: exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output') From d11b0c7460990658176f7355722b7caff0408959 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:29:51 +0000 Subject: [PATCH 10/87] fix: warn when CROSS_ORIGIN_STORAGE is used with SIDE_MODULE SIDE_MODULE builds output only a .wasm file with no JS glue, so there is nothing to embed the hash into or to perform the COS lookup at runtime. Add a warning to make this explicit. Also corrects the earlier reasoning about SPLIT_MODULE: the secondary .deferred.wasm files are produced by a user-run offline wasm-split step with profiling data, not during the Emscripten link, so they genuinely cannot be hashed at link time. --- tools/link.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/link.py b/tools/link.py index 9487cc2f6e1e5..cb72f29a66e81 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1218,6 +1218,8 @@ def limit_incoming_module_api(): diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') if settings.MAIN_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') + if settings.SIDE_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML: exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output') From a0dceab66fbb2e0a728cda194c858959c92e97d1 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 09:54:45 +0000 Subject: [PATCH 11/87] docs: clarify that COS only makes sense for widely-shared public Wasm binaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The key insight is that COS only delivers a benefit when the .wasm binary is byte-identical across many origins — i.e. a publicly distributed library (SQLite Wasm, Pyodide, CanvasKit, ffmpeg.wasm) served from a CDN. Application-specific Wasm gains nothing that the HTTP cache does not already provide, because no other origin will ever have the same hash. - src/settings.js: lead the CROSS_ORIGIN_STORAGE comment with a 'When to use this flag' paragraph listing good candidates and explicitly warning against using it for app-specific Wasm. - src/preamble.js: add the same guidance to the runtime comment block. - CrossOriginStorage.rst: add a 'When to use this flag' subsection before Usage with a concrete list of good candidates and a clear 'Do not' statement for application-specific code. --- .../docs/compiling/CrossOriginStorage.rst | 27 ++++++++++++++++--- src/preamble.js | 9 +++++-- src/settings.js | 12 +++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index ce3e298159ed1..6e2807349253d 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -28,10 +28,29 @@ tries to retrieve the compiled Wasm module from COS before falling back to a normal network fetch. If the module is not yet in COS it is stored there after download, making it available to other origins immediately. -This is particularly beneficial for popular Wasm modules that many sites -ship independently — game engines, scientific computing runtimes, and -frameworks such as Flutter or Pyodide — where users would otherwise download -the same bytes many times. +When to use this flag +--------------------- + +COS only delivers a benefit when the ``.wasm`` binary is **byte-identical +across many different origins** — that is, a publicly distributed library +that many sites load from the same CDN URL. If every visitor to every site +downloads the exact same bytes, COS means they only download it once, ever. + +Good candidates: + +- **SQLite Wasm** — the same ``sqlite3.wasm`` build is loaded by many + independent sites. +- **Pyodide** — ``pyodide.asm.wasm`` is a large, stable binary served from + a public CDN and used across many origins. +- **CanvasKit (Flutter)** — ``canvaskit.wasm`` is requested hundreds of + thousands of times daily from thousands of distinct hosts. +- **ffmpeg.wasm**, **libsodium.wasm**, **WebR** — similarly widely shared, + version-stable, CDN-distributed binaries. + +**Do not** enable this flag for application-specific Wasm code built for +your own site. That binary is unique to you; no other origin will ever have +the same hash, so it will never get a COS cache hit. The normal HTTP cache +already handles per-origin caching efficiently. Usage ===== diff --git a/src/preamble.js b/src/preamble.js index 6d78d19f0b249..8a50b094b56e5 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -631,6 +631,11 @@ async function instantiateAsync(binary, binaryFile, imports) { // Cross-Origin Storage (COS) progressive enhancement. // https://github.com/WICG/cross-origin-storage // + // COS is only beneficial when this .wasm binary is byte-identical across + // many origins — i.e. a publicly distributed library (SQLite Wasm, Pyodide, + // CanvasKit, ffmpeg.wasm, …) fetched from a CDN. Application-specific Wasm + // gains nothing from COS that the normal HTTP cache does not already provide. + // // The SHA-256 hash of the final .wasm binary is computed at link time and // embedded here as a build-time constant. At runtime we feature-detect the // browser COS API via `'crossOriginStorage' in navigator`, then call @@ -642,8 +647,8 @@ async function instantiateAsync(binary, binaryFile, imports) { // // Cache-miss path (NotFoundError): // fetch() the wasm over the network → instantiate → store in COS with - // origins:'*' (appropriate for a public Wasm module that any site may - // benefit from). The store is fire-and-forget so it never delays startup. + // origins:'*' so any origin can reuse the same public binary. + // The store is fire-and-forget so it never delays startup. // // Any other error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. diff --git a/src/settings.js b/src/settings.js index 0fff67409108e..cff867d6fb872 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2208,6 +2208,18 @@ var CROSS_ORIGIN = false; // [experimental] Enables Cross-Origin Storage (COS) API support for Wasm // loading on the Web target. // +// **When to use this flag** +// +// COS is only beneficial for Wasm binaries that are byte-identical across +// many different origins — i.e. publicly distributed libraries fetched from +// a CDN, where the same compiled binary is loaded by thousands of sites. +// Good examples: SQLite Wasm, Pyodide, CanvasKit (Flutter), ffmpeg.wasm, +// libsodium.wasm. +// +// If your ``.wasm`` file is bespoke application code built specifically for +// your site, COS gives you nothing that the normal HTTP cache does not +// already provide. Do not enable this flag for application-specific Wasm. +// // When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` // binary at link time, embeds it as a build-time constant in the generated // JavaScript glue, and uses the browser COS API as a progressive enhancement: From d384b9184133b9850a95c635eb22b0bcf35a14d5 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 17:47:02 +0000 Subject: [PATCH 12/87] feat: add CROSS_ORIGIN_STORAGE_ORIGINS setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exposes the 'origins' field of the COS API's CrossOriginStorageRequestFileHandleOptions dictionary as a new -sCROSS_ORIGIN_STORAGE_ORIGINS linker setting. Three modes, matching the spec: ['*'] (default) — globally available; any origin can read the file. Appropriate for widely-shared public CDN assets (SQLite Wasm, Pyodide, CanvasKit, …). ['https://app.example.com', 'https://api.example.com'] — restricted to a specific set of trusted HTTPS origins. For proprietary resources shared across related sites without making them globally enumerable. [] — same-site only; the 'origins' field is omitted entirely. The file is available only to same-site origins. Link-time validation: - '*' must not be mixed with explicit origins (error) - each explicit origin must match https://host[:port] with no path (error) preamble.js emits the correct JS literal via {{{ JSON.stringify(...) }}}: - ['*'] → origins: '*' - ['https://...'] → origins: ["https://..."] - [] → { create: true } (no origins key) Tests (test/test_other.py): - default emits origins: '*' - explicit list emits a JS array - empty list emits { create: true } with no origins key - error on '*' mixed with explicit origins - error on non-HTTPS origin - error on origin with a path component Docs (CrossOriginStorage.rst): new subsection with all three modes, example command lines, and a note on the spec's visibility upgrade rule. ChangeLog.md updated. --- ChangeLog.md | 3 + .../docs/compiling/CrossOriginStorage.rst | 51 +++++++++++++++ src/preamble.js | 7 +- src/settings.js | 38 +++++++++++ test/test_other.py | 65 +++++++++++++++++++ tools/link.py | 13 ++++ 6 files changed, 176 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 748c364d70496..6901b943e7d3f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -50,6 +50,9 @@ See docs/process.md for more on how version tagging works. ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` (hard error — the wasm is inlined directly into the JS output with no fetchable URL) and ``-sWASM_ASYNC_COMPILATION=0`` (warning). + The companion ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` setting controls which + origins may read the cached file: ``['*']`` (default, globally available), + an explicit HTTPS origin list (restricted), or ``[]`` (same-site only). 6.0.0 - 06/04/26 ---------------- diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 6e2807349253d..43f3b3395f778 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -62,6 +62,57 @@ Pass ``-sCROSS_ORIGIN_STORAGE=1`` at link time:: The flag is a **link-time** setting and has no effect during compilation of individual object files. +Controlling which origins can read the cached file +-------------------------------------------------- + +The ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` setting controls the ``origins`` field +passed to ``requestFileHandles()`` on the write (cache-miss) path. It has no +effect on the read (cache-hit) path. Three modes are available: + +**Globally available** (default) — any origin can retrieve the file: + +.. code-block:: bash + + emcc hello.cpp -o hello.js \ + -sCROSS_ORIGIN_STORAGE=1 \ + -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] + +Use this for widely-shared public binaries distributed from a CDN (SQLite +Wasm, Pyodide, CanvasKit, …). This is the recommended mode for resources +where global COS cache hits are expected. + +**Restricted to a specific set of origins** — only the listed origins can +retrieve the file: + +.. code-block:: bash + + emcc hello.cpp -o hello.js \ + -sCROSS_ORIGIN_STORAGE=1 \ + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://app.example.com","https://api.example.com"]' + +Use this for proprietary resources shared across a controlled set of related +sites. Each entry must be a valid serialised HTTPS origin (scheme + host + +optional port, no path). Mixing ``'*'`` with explicit origins is a +**link-time error**. + +**Same-site only** — the ``origins`` field is omitted, so the file is +available only to same-site origins: + +.. code-block:: bash + + emcc hello.cpp -o hello.js \ + -sCROSS_ORIGIN_STORAGE=1 \ + -sCROSS_ORIGIN_STORAGE_ORIGINS=[] + +Use this for resources that should be shared across subdomains of a single +site but not beyond. + +.. note:: + The COS spec defines a **visibility upgrade** rule: a resource's + availability can be widened but never narrowed. If a resource is already + stored as globally available (``'*'``), any subsequent attempt to store it + with a more restrictive ``origins`` list is ignored by the browser. + Requirements and restrictions ------------------------------ diff --git a/src/preamble.js b/src/preamble.js index 8a50b094b56e5..f296cf6e93bca 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -648,6 +648,7 @@ async function instantiateAsync(binary, binaryFile, imports) { // Cache-miss path (NotFoundError): // fetch() the wasm over the network → instantiate → store in COS with // origins:'*' so any origin can reuse the same public binary. + // (Controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS; see settings.js.) // The store is fire-and-forget so it never delays startup. // // Any other error (NotAllowedError, network failure, …) falls through to the @@ -678,7 +679,11 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var writeHandles = await navigator.crossOriginStorage.requestFileHandles( [cosHash], - { create: true, origins: '*' }, +#if CROSS_ORIGIN_STORAGE_ORIGINS.length + { create: true, origins: {{{ JSON.stringify(CROSS_ORIGIN_STORAGE_ORIGINS.length === 1 && CROSS_ORIGIN_STORAGE_ORIGINS[0] === '*' ? '*' : CROSS_ORIGIN_STORAGE_ORIGINS) }}} }, +#else + { create: true }, +#endif ); var writable = await writeHandles[0].createWritable(); await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); diff --git a/src/settings.js b/src/settings.js index cff867d6fb872..9a4ad0c5c6f32 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2244,6 +2244,44 @@ var CROSS_ORIGIN = false; // [link] var CROSS_ORIGIN_STORAGE = 0; +// Controls which origins may read the Wasm binary after it has been stored in +// the Cross-Origin Storage (COS) cache. Only meaningful when +// ``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) +// path; it is only applied during the write (cache-miss) path. +// +// Three modes are supported, matching the ``origins`` field of the COS API's +// ``CrossOriginStorageRequestFileHandleOptions`` dictionary: +// +// **Globally available** (default) — any origin can retrieve the file:: +// +// -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] +// +// This is appropriate for widely-used public binaries (SQLite Wasm, Pyodide, +// CanvasKit, …) distributed from a public CDN where many unrelated sites load +// the exact same bytes. The explainer recommends ``origins: '*'`` for such +// resources. +// +// **Restricted to a specific set of origins** — only listed origins can +// retrieve the file:: +// +// -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] +// +// Useful when a proprietary Wasm binary is shared across a controlled set of +// related sites (for example, two subdomains of the same company) but should +// not be globally enumerable. Each value must be a valid serialized HTTPS +// origin (scheme + host + optional port, no path). +// +// **Same-site only** — omit the ``origins`` field entirely so the file is +// available only to same-site origins:: +// +// -sCROSS_ORIGIN_STORAGE_ORIGINS=[] +// +// Useful for resources shared across subdomains of a single site. +// +// Mixing ``'*'`` with explicit origins is a link-time error. +// [link] +var CROSS_ORIGIN_STORAGE_ORIGINS = ['*']; + // This setting changes the behaviour of the ``-shared`` flag. When set to true // you get the old emscripten behaviour where the ``-shared`` flag actually // produces a normal object file (i.e. ``ld -r``). When set to true (the diff --git a/test/test_other.py b/test/test_other.py index 272dab95db401..1944fdd2fcd1f 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15706,3 +15706,68 @@ def test_cross_origin_storage_hash_changes_with_content(self): self.assertNotEqual(hash_a, hash_b, 'different programs should produce different WASM_SHA256 hashes') + # --------------------------------------------------------------------------- + # Tests for CROSS_ORIGIN_STORAGE_ORIGINS + # --------------------------------------------------------------------------- + + def test_cross_origin_storage_origins_default_is_global(self): + """Default CROSS_ORIGIN_STORAGE_ORIGINS=['*'] must emit origins:'*'.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-o', 'hello.js']) + self.assertContained("origins: '*'", read_file('hello.js')) + + def test_cross_origin_storage_origins_explicit_list(self): + """An explicit origins list must be emitted as a JS array.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://app.example.com","https://api.example.com"]', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertContained('"https://app.example.com"', js) + self.assertContained('"https://api.example.com"', js) + self.assertNotContained("origins: '*'", js) + + def test_cross_origin_storage_origins_same_site(self): + """Empty origins list must omit the origins key entirely (same-site only).""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=[]', + '-o', 'hello.js']) + js = read_file('hello.js') + # { create: true } with no origins field + self.assertContained('{ create: true }', js) + self.assertNotContained('origins', js) + + def test_cross_origin_storage_origins_error_mixed_wildcard(self): + """Mixing '*' with explicit origins must be a link-time error.""" + self.assert_fail( + [EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]', + '-o', 'hello.js'], + "'*' must not be mixed with explicit origins") + + def test_cross_origin_storage_origins_error_invalid_origin(self): + """A non-HTTPS or malformed origin must be a link-time error.""" + self.assert_fail( + [EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]', + '-o', 'hello.js'], + 'is not a valid HTTPS origin') + + def test_cross_origin_storage_origins_error_origin_with_path(self): + """An origin with a path component must be a link-time error.""" + self.assert_fail( + [EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]', + '-o', 'hello.js'], + 'is not a valid HTTPS origin') diff --git a/tools/link.py b/tools/link.py index cb72f29a66e81..716164bbc3341 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1220,6 +1220,19 @@ def limit_incoming_module_api(): diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') if settings.SIDE_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') + # Validate CROSS_ORIGIN_STORAGE_ORIGINS. + origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS + if not isinstance(origins, list): + exit_with_error('CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. [\'*\'] or [\'https://example.com\']') + if '*' in origins and len(origins) > 1: + exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") + for o in origins: + if o == '*': + continue + # Each explicit origin must be a valid serialised HTTPS origin: + # scheme "https://", host, optional ":port", no path/query/fragment. + if not re.fullmatch(r'https://[^/]+(:\d+)?', o): + exit_with_error(f"CROSS_ORIGIN_STORAGE_ORIGINS: {o!r} is not a valid HTTPS origin (expected 'https://host' or 'https://host:port')") if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML: exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output') From b0a036082246c38eab42eb2aa0168ec515ef7e1d Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 17:52:18 +0000 Subject: [PATCH 13/87] fix: make '*' the implicit default for CROSS_ORIGIN_STORAGE_ORIGINS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the default value was ['*'] in settings.js, which made it impossible to distinguish 'user did not pass the setting' from 'user explicitly passed ["*"]'. Now: - Default in settings.js is [] (empty sentinel) - link.py checks user_settings: if CROSS_ORIGIN_STORAGE_ORIGINS was not explicitly passed, it is resolved to ['*'] at link time (globally available — the right default for a public Wasm binary) - Explicitly passing =[] means same-site only (origins field omitted) - Explicitly passing =['https://...'] means restricted list This means the common case requires no extra flags: emcc -sCROSS_ORIGIN_STORAGE=1 → origins: '*' Docs and tests updated to reflect the new sentinel semantics. An extra test asserts that explicitly passing ['*'] gives the same result as the implicit default. --- .../docs/compiling/CrossOriginStorage.rst | 13 ++++---- src/settings.js | 32 ++++++++----------- test/test_other.py | 13 +++++++- tools/link.py | 10 ++++-- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 43f3b3395f778..6873b42debdd7 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -69,13 +69,14 @@ The ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` setting controls the ``origins`` field passed to ``requestFileHandles()`` on the write (cache-miss) path. It has no effect on the read (cache-hit) path. Three modes are available: -**Globally available** (default) — any origin can retrieve the file: +**Globally available** (default, no explicit setting needed) — any origin +can retrieve the file. This is applied automatically when +``-sCROSS_ORIGIN_STORAGE=1`` is used without specifying +``-sCROSS_ORIGIN_STORAGE_ORIGINS``: .. code-block:: bash - emcc hello.cpp -o hello.js \ - -sCROSS_ORIGIN_STORAGE=1 \ - -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] + emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 Use this for widely-shared public binaries distributed from a CDN (SQLite Wasm, Pyodide, CanvasKit, …). This is the recommended mode for resources @@ -95,8 +96,8 @@ sites. Each entry must be a valid serialised HTTPS origin (scheme + host + optional port, no path). Mixing ``'*'`` with explicit origins is a **link-time error**. -**Same-site only** — the ``origins`` field is omitted, so the file is -available only to same-site origins: +**Same-site only** — pass an explicit empty list to omit the ``origins`` +field, making the file available only to same-site origins: .. code-block:: bash diff --git a/src/settings.js b/src/settings.js index 9a4ad0c5c6f32..36317cc3a4aa6 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2252,35 +2252,29 @@ var CROSS_ORIGIN_STORAGE = 0; // Three modes are supported, matching the ``origins`` field of the COS API's // ``CrossOriginStorageRequestFileHandleOptions`` dictionary: // -// **Globally available** (default) — any origin can retrieve the file:: +// **Globally available** (default when the setting is not explicitly passed) — +// any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is +// used without specifying this setting, ``origins: '*'`` is used automatically. +// Appropriate for widely-used public binaries (SQLite Wasm, Pyodide, +// CanvasKit, …) distributed from a public CDN. // -// -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] -// -// This is appropriate for widely-used public binaries (SQLite Wasm, Pyodide, -// CanvasKit, …) distributed from a public CDN where many unrelated sites load -// the exact same bytes. The explainer recommends ``origins: '*'`` for such -// resources. -// -// **Restricted to a specific set of origins** — only listed origins can +// **Restricted to a specific set of origins** — only listed HTTPS origins can // retrieve the file:: // // -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] // -// Useful when a proprietary Wasm binary is shared across a controlled set of -// related sites (for example, two subdomains of the same company) but should -// not be globally enumerable. Each value must be a valid serialized HTTPS -// origin (scheme + host + optional port, no path). +// For proprietary resources shared across a controlled set of related sites. +// Each value must be a valid serialised HTTPS origin (scheme + host + optional +// port, no path). Mixing ``'*'`` with explicit origins is a link-time error. // -// **Same-site only** — omit the ``origins`` field entirely so the file is -// available only to same-site origins:: +// **Same-site only** — pass the setting with an empty list to omit the +// ``origins`` field, making the file available only to same-site origins:: // // -sCROSS_ORIGIN_STORAGE_ORIGINS=[] // -// Useful for resources shared across subdomains of a single site. -// -// Mixing ``'*'`` with explicit origins is a link-time error. +// For resources shared across subdomains of a single site but not beyond. // [link] -var CROSS_ORIGIN_STORAGE_ORIGINS = ['*']; +var CROSS_ORIGIN_STORAGE_ORIGINS = []; // This setting changes the behaviour of the ``-shared`` flag. When set to true // you get the old emscripten behaviour where the ``-shared`` flag actually diff --git a/test/test_other.py b/test/test_other.py index 1944fdd2fcd1f..41dd9f5e75ce8 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15711,13 +15711,24 @@ def test_cross_origin_storage_hash_changes_with_content(self): # --------------------------------------------------------------------------- def test_cross_origin_storage_origins_default_is_global(self): - """Default CROSS_ORIGIN_STORAGE_ORIGINS=['*'] must emit origins:'*'.""" + """Without -sCROSS_ORIGIN_STORAGE_ORIGINS, the default must be origins:'*' + (globally available). The user only needs -sCROSS_ORIGIN_STORAGE=1.""" self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', '-o', 'hello.js']) self.assertContained("origins: '*'", read_file('hello.js')) + def test_cross_origin_storage_origins_explicit_wildcard(self): + """Explicitly passing -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] must also emit + origins:'*', matching the implicit default.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + "-sCROSS_ORIGIN_STORAGE_ORIGINS=['*']", + '-o', 'hello.js']) + self.assertContained("origins: '*'", read_file('hello.js')) + def test_cross_origin_storage_origins_explicit_list(self): """An explicit origins list must be emitted as a JS array.""" self.run_process([EMCC, test_file('hello_world.cpp'), diff --git a/tools/link.py b/tools/link.py index 716164bbc3341..19462aa23e7ec 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1220,10 +1220,16 @@ def limit_incoming_module_api(): diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') if settings.SIDE_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') - # Validate CROSS_ORIGIN_STORAGE_ORIGINS. + # Resolve and validate CROSS_ORIGIN_STORAGE_ORIGINS. + # The default in settings.js is [] (empty sentinel). When the user has + # not explicitly passed -sCROSS_ORIGIN_STORAGE_ORIGINS we default to ['*'] + # (globally available), which is the appropriate mode for widely-shared + # public Wasm binaries. An explicit =[] means same-site only. + if 'CROSS_ORIGIN_STORAGE_ORIGINS' not in user_settings: + settings.CROSS_ORIGIN_STORAGE_ORIGINS = ['*'] origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS if not isinstance(origins, list): - exit_with_error('CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. [\'*\'] or [\'https://example.com\']') + exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. ['*'] or ['https://example.com']") if '*' in origins and len(origins) > 1: exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") for o in origins: From b6ace778cd2e595395c405ad41ce61e7a72c0cc1 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 18:00:05 +0000 Subject: [PATCH 14/87] =?UTF-8?q?fix:=20final=20polish=20=E2=80=94=20warni?= =?UTF-8?q?ngs,=20stale=20comments,=20doc=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/link.py - Add warning when CROSS_ORIGIN_STORAGE=1 is used with a non-web environment (ENVIRONMENT=node, shell, etc.): navigator.crossOriginStorage is a browser API and is never available outside the browser. This makes the non-web case consistent with all other no-op combinations which already warn (WASM_ASYNC_COMPILATION=0, SPLIT_MODULE, MAIN_MODULE, SIDE_MODULE). - Fix stale 'Inline / SINGLE_FILE builds' comment in the hash computation else-branch: SINGLE_FILE is now a hard error so that branch is only reached in unexpected build configurations. src/preamble.js - Fix stale comment that hardcoded origins:'*'; replace with a reference to -sCROSS_ORIGIN_STORAGE_ORIGINS so the comment stays accurate regardless of the setting value. test/test_other.py - Update test_cross_origin_storage_not_emitted_for_node_target to also assert the new warning is emitted (matching the pattern of all other warning tests). CrossOriginStorage.rst - 'silently ignored' → 'emits a warning' for non-web targets. - Fix stale origins:'*' hardcode in the 'How it works / Cache miss' step; now references -sCROSS_ORIGIN_STORAGE_ORIGINS instead. --- .../docs/compiling/CrossOriginStorage.rst | 13 +++++++------ src/preamble.js | 8 ++++---- test/test_other.py | 17 ++++++++++------- tools/link.py | 7 ++++++- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 6873b42debdd7..43bbe376af58d 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -117,8 +117,10 @@ site but not beyond. Requirements and restrictions ------------------------------ -- The flag only has an effect when the output targets the **web** environment. - It is silently ignored for Node.js-only or shell targets (``-sENVIRONMENT=node``). +- The flag emits a **warning** when the target environment does not include + the web (``-sENVIRONMENT=node``, ``-sENVIRONMENT=shell``): + ``navigator.crossOriginStorage`` is a browser API and is never available + in those environments. - It produces a **hard link-time error** in **SINGLE_FILE** mode (``-sSINGLE_FILE``): the Wasm binary is embedded directly into the JS output and has no standalone ``.wasm`` file or fetchable URL to key the @@ -169,10 +171,9 @@ When the page loads, the generated JavaScript follows this logic: over the network as usual, invoke ``Module['onCOSCacheMiss'](url)`` if defined, call ``WebAssembly.instantiate()`` immediately so the page loads without delay, and then write the bytes into COS in the background - (fire-and-forget) with ``origins: '*'`` so any other origin can benefit. - Once the write completes, invoke ``Module['onCOSStore'](hash)`` if defined:: - - navigator.crossOriginStorage.requestFileHandles([hash], { create: true, origins: '*' }) + (fire-and-forget) using the ``origins`` value controlled by + ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` (``'*'`` by default). + Once the write completes, invoke ``Module['onCOSStore'](hash)`` if defined. 4. **Fallback** — any unexpected error (``NotAllowedError`` from the browser, network failure during the miss path, etc.) is logged with ``err()`` and diff --git a/src/preamble.js b/src/preamble.js index f296cf6e93bca..02576139697e2 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -646,10 +646,10 @@ async function instantiateAsync(binary, binaryFile, imports) { // requestFileHandles() succeeds → getFile() → arrayBuffer() → instantiate // // Cache-miss path (NotFoundError): - // fetch() the wasm over the network → instantiate → store in COS with - // origins:'*' so any origin can reuse the same public binary. - // (Controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS; see settings.js.) - // The store is fire-and-forget so it never delays startup. + // fetch() the wasm over the network → instantiate → store in COS. + // The origins field is controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS + // (default: '*', globally available). The store is fire-and-forget so + // it never delays startup. // // Any other error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. diff --git a/test/test_other.py b/test/test_other.py index 41dd9f5e75ce8..459d276036666 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15637,13 +15637,16 @@ def test_cross_origin_storage_disabled_by_default(self): def test_cross_origin_storage_not_emitted_for_node_target(self): """COS code must NOT appear when targeting Node.js only, even with the flag - set; the #if ENVIRONMENT_MAY_BE_WEB guard should strip it.""" - self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', - '-sENVIRONMENT=node', - '-o', 'hello.js']) - js = read_file('hello.js') - self.assertNotContained('crossOriginStorage', js) + set; the #if ENVIRONMENT_MAY_BE_WEB guard strips it. A warning must also + be emitted since the flag does nothing in this configuration.""" + proc = self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=node', + '-o', 'hello.js'], + stderr=PIPE) + self.assertNotContained('crossOriginStorage', read_file('hello.js')) + self.assertContained('CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web', + proc.stderr) def test_cross_origin_storage_error_with_single_file(self): """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" diff --git a/tools/link.py b/tools/link.py index 19462aa23e7ec..5f35926204e48 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1212,6 +1212,8 @@ def limit_incoming_module_api(): if settings.CROSS_ORIGIN_STORAGE: if settings.SINGLE_FILE: exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on)') + if not settings.ENVIRONMENT_MAY_BE_WEB: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web (navigator.crossOriginStorage is not available outside the browser)') if not settings.WASM_ASYNC_COMPILATION: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') if settings.SPLIT_MODULE: @@ -1943,7 +1945,10 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat settings.WASM_SHA256 = hashlib.sha256(wasm_bytes).hexdigest() logger.debug(f'CROSS_ORIGIN_STORAGE: wasm SHA-256 = {settings.WASM_SHA256}') else: - # Inline / SINGLE_FILE builds embed the wasm in JS; no file to hash. + # wasm_target does not exist — this should not normally be reached since + # SINGLE_FILE (which inlines the wasm) is already rejected above with a + # hard error. Log a warning defensively in case of an unexpected build + # configuration. logger.warning('CROSS_ORIGIN_STORAGE: wasm file not found for hashing; WASM_SHA256 will be empty') settings.WASM_SHA256 = '' From 8d4500a047d69919de43b430148682c9cad0fb01 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 6 Jun 2026 18:03:42 +0000 Subject: [PATCH 15/87] test: add missing SIDE_MODULE warning test for CROSS_ORIGIN_STORAGE The warning was implemented in tools/link.py but never tested. Added test_cross_origin_storage_warning_with_side_module to assert the expected message appears in stderr. --- test/test_other.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/test_other.py b/test/test_other.py index 459d276036666..e04f02b10e41f 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15690,6 +15690,16 @@ def test_cross_origin_storage_warning_with_main_module(self): self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', proc.stderr) + def test_cross_origin_storage_warning_with_side_module(self): + """CROSS_ORIGIN_STORAGE + SIDE_MODULE must warn: no JS glue is emitted.""" + proc = self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sSIDE_MODULE', + '-o', 'hello.wasm'], + stderr=PIPE) + self.assertContained('CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds', + proc.stderr) + def test_cross_origin_storage_hash_changes_with_content(self): """Two different programs must produce different embedded hashes.""" self.run_process([EMCC, test_file('hello_world.cpp'), From 1d34d42e804aaa4cf9fa934867fce31bbd9823e6 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 10:16:38 +0200 Subject: [PATCH 16/87] fix: correct COS test failures and add demo Makefile --- src/preamble.js | 18 ++++++++------ test/cross_origin_storage/Makefile | 21 ++++++++++++++++ test/test_other.py | 2 +- tools/link.py | 40 +++++++++++++++++++----------- 4 files changed, 58 insertions(+), 23 deletions(-) create mode 100644 test/cross_origin_storage/Makefile diff --git a/src/preamble.js b/src/preamble.js index 02576139697e2..196fe86cb7cef 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -653,9 +653,8 @@ async function instantiateAsync(binary, binaryFile, imports) { // // Any other error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. - var wasmHashValue = '{{{ WASM_SHA256 }}}'; - if (wasmHashValue && 'crossOriginStorage' in navigator) { - var cosHash = { algorithm: 'SHA-256', value: wasmHashValue }; + var cosHash = { algorithm: 'SHA-256', value: '{{{ WASM_SHA256 }}}' }; + if (cosHash.value && 'crossOriginStorage' in navigator) { try { var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); // Cache hit — read the Blob and instantiate from its ArrayBuffer. @@ -663,7 +662,7 @@ async function instantiateAsync(binary, binaryFile, imports) { var cosBytes = await cosFile.arrayBuffer(); // Optional instrumentation callback: Module['onCOSCacheHit'](hash) // Called when the Wasm binary is served from the cross-origin cache. - if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](wasmHashValue); + if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](cosHash.value); return WebAssembly.instantiate(cosBytes, imports); } catch (cosErr) { if (cosErr.name === 'NotFoundError') { @@ -679,8 +678,10 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var writeHandles = await navigator.crossOriginStorage.requestFileHandles( [cosHash], -#if CROSS_ORIGIN_STORAGE_ORIGINS.length - { create: true, origins: {{{ JSON.stringify(CROSS_ORIGIN_STORAGE_ORIGINS.length === 1 && CROSS_ORIGIN_STORAGE_ORIGINS[0] === '*' ? '*' : CROSS_ORIGIN_STORAGE_ORIGINS) }}} }, +#if CROSS_ORIGIN_STORAGE_ORIGINS.length === 1 && CROSS_ORIGIN_STORAGE_ORIGINS[0] === '*' + { create: true, origins: '*' }, +#elif CROSS_ORIGIN_STORAGE_ORIGINS.length + { create: true, origins: {{{ JSON.stringify(CROSS_ORIGIN_STORAGE_ORIGINS) }}} }, #else { create: true }, #endif @@ -690,7 +691,7 @@ async function instantiateAsync(binary, binaryFile, imports) { await writable.close(); // Optional instrumentation callback: Module['onCOSStore'](hash) // Called after the Wasm binary has been successfully written to COS. - if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](wasmHashValue); + if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](cosHash.value); } catch (storeErr) { err(`COS store failed: ${storeErr}`); } @@ -700,8 +701,9 @@ async function instantiateAsync(binary, binaryFile, imports) { // Network fetch failed; fall through to the standard path. err(`COS fallback fetch failed: ${fetchErr}`); } + } else if (cosErr.name === 'NotAllowedError') { + err(`COS: permission denied.`); } else { - // NotAllowedError or unexpected error; fall through gracefully. err(`Cross-Origin Storage lookup failed: ${cosErr}`); } } diff --git a/test/cross_origin_storage/Makefile b/test/cross_origin_storage/Makefile new file mode 100644 index 0000000000000..0c5e4216bb8bf --- /dev/null +++ b/test/cross_origin_storage/Makefile @@ -0,0 +1,21 @@ +# Copyright 2026 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. + +EMCC ?= emcc + +.PHONY: all clean + +all: index.js + +index.js: main.cpp + $(EMCC) main.cpp -o index.js \ + -sCROSS_ORIGIN_STORAGE=1 \ + -sENVIRONMENT=web \ + -sEXPORTED_RUNTIME_METHODS=ccall \ + -sEXPORTED_FUNCTIONS=_greet \ + -sALLOW_MEMORY_GROWTH + +clean: + rm -f index.js index.wasm diff --git a/test/test_other.py b/test/test_other.py index e04f02b10e41f..1ff9adcb1a285 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15764,7 +15764,7 @@ def test_cross_origin_storage_origins_same_site(self): js = read_file('hello.js') # { create: true } with no origins field self.assertContained('{ create: true }', js) - self.assertNotContained('origins', js) + self.assertNotContained('origins:', js) def test_cross_origin_storage_origins_error_mixed_wildcard(self): """Mixing '*' with explicit origins must be a link-time error.""" diff --git a/tools/link.py b/tools/link.py index 5f35926204e48..3726afde77dcb 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1922,6 +1922,20 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat bindgen_jslib = building.run_wasm_bindgen(in_wasm) settings.JS_LIBRARIES.append(bindgen_jslib) + # Compute the SHA-256 of the wasm binary before phase_emscript so the hash + # is available to the JS-glue template as {{{ WASM_SHA256 }}}. We read + # in_wasm here (the pre-binaryen binary); after phase_binaryen we recompute + # the hash and patch the JS if binaryen changed the binary. + if settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB: + if os.path.exists(in_wasm): + with open(in_wasm, 'rb') as f: + wasm_bytes_pre = f.read() + settings.WASM_SHA256 = hashlib.sha256(wasm_bytes_pre).hexdigest() + logger.debug(f'CROSS_ORIGIN_STORAGE: pre-binaryen wasm SHA-256 = {settings.WASM_SHA256}') + else: + logger.warning('CROSS_ORIGIN_STORAGE: wasm file not found for hashing; WASM_SHA256 will be empty') + settings.WASM_SHA256 = '' + metadata = phase_emscript(in_wasm, wasm_target, js_syms, base_metadata) if settings.EMBIND_AOT: @@ -1935,22 +1949,20 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat phase_binaryen(target, options, wasm_target) - # Compute the SHA-256 of the final .wasm binary and make it available to the - # JS glue preprocessor as {{{ WASM_SHA256 }}}. We do this after phase_binaryen - # so that wasm-opt transformations are already reflected in the hash. - if settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB: + # After binaryen, recompute the hash from the final wasm and patch the JS + # if binaryen changed the binary (so the embedded hash stays accurate). + if final_js and settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB and settings.WASM_SHA256: if os.path.exists(wasm_target): with open(wasm_target, 'rb') as f: - wasm_bytes = f.read() - settings.WASM_SHA256 = hashlib.sha256(wasm_bytes).hexdigest() - logger.debug(f'CROSS_ORIGIN_STORAGE: wasm SHA-256 = {settings.WASM_SHA256}') - else: - # wasm_target does not exist — this should not normally be reached since - # SINGLE_FILE (which inlines the wasm) is already rejected above with a - # hard error. Log a warning defensively in case of an unexpected build - # configuration. - logger.warning('CROSS_ORIGIN_STORAGE: wasm file not found for hashing; WASM_SHA256 will be empty') - settings.WASM_SHA256 = '' + wasm_bytes_post = f.read() + post_hash = hashlib.sha256(wasm_bytes_post).hexdigest() + if post_hash != settings.WASM_SHA256: + logger.debug(f'CROSS_ORIGIN_STORAGE: binaryen changed wasm; updating SHA-256 to {post_hash}') + js_content = read_file(final_js) + js_content = js_content.replace(f"value: '{settings.WASM_SHA256}'", + f"value: '{post_hash}'") + write_file(final_js, js_content) + settings.WASM_SHA256 = post_hash # If we are not emitting any JS then we are all done now if options.oformat != OFormat.WASM: From 4732ad42a84065a05be89ffaae048ff46fc61daa Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 10:31:16 +0200 Subject: [PATCH 17/87] docs: replace concrete project examples with generic descriptions --- .../docs/compiling/CrossOriginStorage.rst | 19 ++++++++----------- src/preamble.js | 6 +++--- src/settings.js | 8 ++++---- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 43bbe376af58d..6498f2138e3e3 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -36,16 +36,13 @@ across many different origins** — that is, a publicly distributed library that many sites load from the same CDN URL. If every visitor to every site downloads the exact same bytes, COS means they only download it once, ever. -Good candidates: +Good candidates are libraries or toolkits that are: -- **SQLite Wasm** — the same ``sqlite3.wasm`` build is loaded by many - independent sites. -- **Pyodide** — ``pyodide.asm.wasm`` is a large, stable binary served from - a public CDN and used across many origins. -- **CanvasKit (Flutter)** — ``canvaskit.wasm`` is requested hundreds of - thousands of times daily from thousands of distinct hosts. -- **ffmpeg.wasm**, **libsodium.wasm**, **WebR** — similarly widely shared, - version-stable, CDN-distributed binaries. +- distributed from a public CDN as a stable, version-pinned ``.wasm`` binary, +- loaded by many independent sites (i.e. many distinct origins), and +- a **single primary** ``.wasm`` file (COS only covers the binary that + Emscripten compiles; any additional Wasm files loaded at runtime are not + covered). **Do not** enable this flag for application-specific Wasm code built for your own site. That binary is unique to you; no other origin will ever have @@ -78,8 +75,8 @@ can retrieve the file. This is applied automatically when emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 -Use this for widely-shared public binaries distributed from a CDN (SQLite -Wasm, Pyodide, CanvasKit, …). This is the recommended mode for resources +Use this for widely-shared public binaries distributed from a CDN and loaded +by many independent origins. This is the recommended mode for resources where global COS cache hits are expected. **Restricted to a specific set of origins** — only the listed origins can diff --git a/src/preamble.js b/src/preamble.js index 196fe86cb7cef..0f392a603c891 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -632,9 +632,9 @@ async function instantiateAsync(binary, binaryFile, imports) { // https://github.com/WICG/cross-origin-storage // // COS is only beneficial when this .wasm binary is byte-identical across - // many origins — i.e. a publicly distributed library (SQLite Wasm, Pyodide, - // CanvasKit, ffmpeg.wasm, …) fetched from a CDN. Application-specific Wasm - // gains nothing from COS that the normal HTTP cache does not already provide. + // many origins — i.e. a library distributed from a CDN and shared by many + // independent sites. Application-specific Wasm gains nothing from COS that + // the normal HTTP cache does not already provide. // // The SHA-256 hash of the final .wasm binary is computed at link time and // embedded here as a build-time constant. At runtime we feature-detect the diff --git a/src/settings.js b/src/settings.js index 36317cc3a4aa6..5224b5bdf125d 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2213,8 +2213,8 @@ var CROSS_ORIGIN = false; // COS is only beneficial for Wasm binaries that are byte-identical across // many different origins — i.e. publicly distributed libraries fetched from // a CDN, where the same compiled binary is loaded by thousands of sites. -// Good examples: SQLite Wasm, Pyodide, CanvasKit (Flutter), ffmpeg.wasm, -// libsodium.wasm. +// Good candidates are libraries or toolkits distributed from a public CDN +// as a stable, version-pinned binary loaded by many independent sites. // // If your ``.wasm`` file is bespoke application code built specifically for // your site, COS gives you nothing that the normal HTTP cache does not @@ -2255,8 +2255,8 @@ var CROSS_ORIGIN_STORAGE = 0; // **Globally available** (default when the setting is not explicitly passed) — // any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is // used without specifying this setting, ``origins: '*'`` is used automatically. -// Appropriate for widely-used public binaries (SQLite Wasm, Pyodide, -// CanvasKit, …) distributed from a public CDN. +// Appropriate for widely-used public binaries distributed from a CDN +// that are shared across many independent origins. // // **Restricted to a specific set of origins** — only listed HTTPS origins can // retrieve the file:: From d80e53e1df70a0b2d93db7dab3191aacfd43d811 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 11:38:58 +0200 Subject: [PATCH 18/87] docs: add Chrome Web Store link for COS extension in See also --- site/source/docs/compiling/CrossOriginStorage.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 6498f2138e3e3..bbf899133f84c 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -250,6 +250,7 @@ See also ======== - `WICG Cross-Origin Storage explainer `_ -- `COS browser extension (polyfill) `_ +- `COS browser extension (Chrome Web Store) `_ +- `COS browser extension (source code) `_ - :ref:`settings-reference` — ``CROSS_ORIGIN_STORAGE`` entry - :ref:`WebAssembly` — general guide to building Wasm with Emscripten From 6dedaee976f76f9b45e055d4078a6652c54fb661 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 11:40:16 +0200 Subject: [PATCH 19/87] docs: replace CDN framing with 'popular' in COS guide --- site/source/docs/compiling/CrossOriginStorage.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index bbf899133f84c..4e23c7c3a4f37 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -32,14 +32,14 @@ When to use this flag --------------------- COS only delivers a benefit when the ``.wasm`` binary is **byte-identical -across many different origins** — that is, a publicly distributed library -that many sites load from the same CDN URL. If every visitor to every site +across many different origins** — that is, a popular library whose compiled +binary is loaded by many independent sites. If every visitor to every site downloads the exact same bytes, COS means they only download it once, ever. Good candidates are libraries or toolkits that are: -- distributed from a public CDN as a stable, version-pinned ``.wasm`` binary, -- loaded by many independent sites (i.e. many distinct origins), and +- popular enough that many independent sites load the same binary, +- distributed as a stable, version-pinned ``.wasm`` file, and - a **single primary** ``.wasm`` file (COS only covers the binary that Emscripten compiles; any additional Wasm files loaded at runtime are not covered). @@ -75,9 +75,8 @@ can retrieve the file. This is applied automatically when emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 -Use this for widely-shared public binaries distributed from a CDN and loaded -by many independent origins. This is the recommended mode for resources -where global COS cache hits are expected. +Use this for popular binaries loaded by many independent origins. This is +the recommended mode for resources where global COS cache hits are expected. **Restricted to a specific set of origins** — only the listed origins can retrieve the file: From dc8eb06a015b59b139eb1873573d89b2cf4e1674 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 11:42:27 +0200 Subject: [PATCH 20/87] docs: note multi-origin exception and origins flag in COS when-to-use --- site/source/docs/compiling/CrossOriginStorage.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 4e23c7c3a4f37..8701c8b7bd6d0 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -49,6 +49,13 @@ your own site. That binary is unique to you; no other origin will ever have the same hash, so it will never get a COS cache hit. The normal HTTP cache already handles per-origin caching efficiently. +The exception is a Wasm binary that you deploy across **multiple origins you +own** — for example, the same library shared between ``https://app.example.com`` +and ``https://api.example.com``. In that case COS can eliminate the redundant +download between your own origins. Use ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` to +restrict access to only those origins rather than opening the cache entry to +the world. + Usage ===== From cfb4e8c56e989ab93f6012182e4948a49f9c9641 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 11:50:10 +0200 Subject: [PATCH 21/87] feat: pass hash as first arg to onCOSCacheMiss(hash, url) --- site/source/docs/compiling/CrossOriginStorage.rst | 8 ++++---- src/preamble.js | 4 ++-- test/cross_origin_storage/index.html | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 8701c8b7bd6d0..58c83ff708c2c 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -171,7 +171,7 @@ When the page loads, the generated JavaScript follows this logic: Then invoke ``Module['onCOSCacheHit'](hash)`` if defined. 3. **Cache miss** — if a ``NotFoundError`` is thrown, fetch the ``.wasm`` - over the network as usual, invoke ``Module['onCOSCacheMiss'](url)`` if + over the network as usual, invoke ``Module['onCOSCacheMiss'](hash, url)`` if defined, call ``WebAssembly.instantiate()`` immediately so the page loads without delay, and then write the bytes into COS in the background (fire-and-forget) using the ``origins`` value controlled by @@ -197,9 +197,9 @@ Three optional ``Module`` properties let you observe COS events at runtime: }, // Called when the Wasm binary was not in COS and was fetched over the - // network. |url| is the resolved URL of the .wasm file. - onCOSCacheMiss: (url) => { - console.log('Cache miss, fetched from:', url); + // network. |hash| is the SHA-256 that missed; |url| is the fallback URL. + onCOSCacheMiss: (hash, url) => { + console.log('Cache miss, SHA-256:', hash, 'fetched from:', url); }, // Called after the Wasm binary has been successfully written to COS. diff --git a/src/preamble.js b/src/preamble.js index 0f392a603c891..f29531cd06510 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -670,9 +670,9 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); var wasmBytes = await networkResponse.arrayBuffer(); - // Optional instrumentation callback: Module['onCOSCacheMiss'](url) + // Optional instrumentation callback: Module['onCOSCacheMiss'](hash, url) // Called when the Wasm binary is not in COS and is fetched over the network. - if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](binaryFile); + if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](cosHash.value, binaryFile); // Fire-and-forget store; never block instantiation on the write. (async () => { try { diff --git a/test/cross_origin_storage/index.html b/test/cross_origin_storage/index.html index 9b284c699a107..ca7f592187b8f 100644 --- a/test/cross_origin_storage/index.html +++ b/test/cross_origin_storage/index.html @@ -62,8 +62,8 @@

Emscripten — Cross-Origin Storage example

}, // Called when the Wasm binary was not in COS and was fetched over the network. - onCOSCacheMiss: (url) => { - log('↓ Wasm not in Cross-Origin Storage — fetched from network: ' + url, 'warn'); + onCOSCacheMiss: (hash, url) => { + log('↓ Wasm not in Cross-Origin Storage — fetched from network: ' + url + ' (SHA-256: ' + hash + ')', 'warn'); }, // Called after the Wasm binary has been successfully written to COS. From 1fb20dde90400c533c4528e3805354b47f983a43 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 11:57:51 +0200 Subject: [PATCH 22/87] docs: remove CDN mention and add visibility/security note in preamble.js Replace "distributed from a CDN" with "popular library loaded by many independent sites", and add a short note explaining that COS cannot be used as a timing oracle for restricted entries: a cache hit requires an explicit prior write that provided the actual bytes. --- src/preamble.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/preamble.js b/src/preamble.js index f29531cd06510..78eea268d4fda 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -632,9 +632,9 @@ async function instantiateAsync(binary, binaryFile, imports) { // https://github.com/WICG/cross-origin-storage // // COS is only beneficial when this .wasm binary is byte-identical across - // many origins — i.e. a library distributed from a CDN and shared by many - // independent sites. Application-specific Wasm gains nothing from COS that - // the normal HTTP cache does not already provide. + // many origins — i.e. a popular library loaded by many independent sites. + // Application-specific Wasm gains nothing from COS that the normal HTTP + // cache does not already provide. // // The SHA-256 hash of the final .wasm binary is computed at link time and // embedded here as a build-time constant. At runtime we feature-detect the @@ -651,6 +651,13 @@ async function instantiateAsync(binary, binaryFile, imports) { // (default: '*', globally available). The store is fire-and-forget so // it never delays startup. // + // Visibility and security: the spec allows widening visibility (e.g. a + // restricted entry promoted to globally available) but never narrowing it. + // Because storing always requires writing the actual bytes, no third party + // can probe the cache to discover whether a restricted entry was previously + // stored by another origin — a cache hit is only possible after an explicit + // write that provided the content. + // // Any other error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. var cosHash = { algorithm: 'SHA-256', value: '{{{ WASM_SHA256 }}}' }; From 3baba82c3107781837b70ee85a24fc848442569b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:01:20 +0200 Subject: [PATCH 23/87] docs: add timing-oracle security note to visibility upgrade callout in RST --- site/source/docs/compiling/CrossOriginStorage.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 58c83ff708c2c..79556b57884e8 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -117,6 +117,13 @@ site but not beyond. stored as globally available (``'*'``), any subsequent attempt to store it with a more restrictive ``origins`` list is ignored by the browser. + This rule also has a security implication: because storing always requires + writing the actual bytes of the resource, no third party can probe the + cache to determine whether a restricted-origin entry was previously stored + by another origin. A cache hit is only possible after an explicit write + that provided the content, so COS cannot be used as a timing oracle to + detect the presence of a resource that the probing origin cannot access. + Requirements and restrictions ------------------------------ From 1d88a75b73a8dc3adba798d91c33356731ba70e7 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:09:08 +0200 Subject: [PATCH 24/87] docs: remove CDN mentions from CROSS_ORIGIN_STORAGE settings comments --- src/settings.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/settings.js b/src/settings.js index 5224b5bdf125d..6fa396518b0f8 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2211,10 +2211,10 @@ var CROSS_ORIGIN = false; // **When to use this flag** // // COS is only beneficial for Wasm binaries that are byte-identical across -// many different origins — i.e. publicly distributed libraries fetched from -// a CDN, where the same compiled binary is loaded by thousands of sites. -// Good candidates are libraries or toolkits distributed from a public CDN -// as a stable, version-pinned binary loaded by many independent sites. +// many different origins — i.e. popular libraries where the same compiled +// binary is loaded by many independent sites. +// Good candidates are libraries or toolkits distributed as a stable, +// version-pinned binary loaded by many independent sites. // // If your ``.wasm`` file is bespoke application code built specifically for // your site, COS gives you nothing that the normal HTTP cache does not @@ -2255,8 +2255,8 @@ var CROSS_ORIGIN_STORAGE = 0; // **Globally available** (default when the setting is not explicitly passed) — // any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is // used without specifying this setting, ``origins: '*'`` is used automatically. -// Appropriate for widely-used public binaries distributed from a CDN -// that are shared across many independent origins. +// Appropriate for widely-used public binaries shared across many independent +// origins. // // **Restricted to a specific set of origins** — only listed HTTPS origins can // retrieve the file:: From 30760878066e0919a147c6d3efba12f5f38aaf0c Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:11:48 +0200 Subject: [PATCH 25/87] style: normalize double spaces after periods in COS settings comments --- src/settings.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/settings.js b/src/settings.js index 6fa396518b0f8..b16facc67cafb 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2218,7 +2218,7 @@ var CROSS_ORIGIN = false; // // If your ``.wasm`` file is bespoke application code built specifically for // your site, COS gives you nothing that the normal HTTP cache does not -// already provide. Do not enable this flag for application-specific Wasm. +// already provide. Do not enable this flag for application-specific Wasm. // // When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` // binary at link time, embeds it as a build-time constant in the generated @@ -2235,7 +2235,7 @@ var CROSS_ORIGIN = false; // ``fetch`` / ``WebAssembly.instantiateStreaming`` path transparently. // // Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no -// effect on Node.js or shell targets. Also has no effect in SINGLE_FILE +// effect on Node.js or shell targets. Also has no effect in SINGLE_FILE // builds where the Wasm binary is inlined directly into the JS output. // // See :ref:`CrossOriginStorage` for the full guide, including how to test @@ -2245,15 +2245,15 @@ var CROSS_ORIGIN = false; var CROSS_ORIGIN_STORAGE = 0; // Controls which origins may read the Wasm binary after it has been stored in -// the Cross-Origin Storage (COS) cache. Only meaningful when -// ``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) +// the Cross-Origin Storage (COS) cache. Only meaningful when +// ``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) // path; it is only applied during the write (cache-miss) path. // // Three modes are supported, matching the ``origins`` field of the COS API's // ``CrossOriginStorageRequestFileHandleOptions`` dictionary: // // **Globally available** (default when the setting is not explicitly passed) — -// any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is +// any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is // used without specifying this setting, ``origins: '*'`` is used automatically. // Appropriate for widely-used public binaries shared across many independent // origins. @@ -2265,7 +2265,7 @@ var CROSS_ORIGIN_STORAGE = 0; // // For proprietary resources shared across a controlled set of related sites. // Each value must be a valid serialised HTTPS origin (scheme + host + optional -// port, no path). Mixing ``'*'`` with explicit origins is a link-time error. +// port, no path). Mixing ``'*'`` with explicit origins is a link-time error. // // **Same-site only** — pass the setting with an empty list to omit the // ``origins`` field, making the file available only to same-site origins:: From b4f85034a9ba1f676e8c1eace568251a3ee72627 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:14:57 +0200 Subject: [PATCH 26/87] style: use American spelling 'serialized' consistently --- site/source/docs/compiling/CrossOriginStorage.rst | 2 +- src/settings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 79556b57884e8..947385e6f734b 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -95,7 +95,7 @@ retrieve the file: '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://app.example.com","https://api.example.com"]' Use this for proprietary resources shared across a controlled set of related -sites. Each entry must be a valid serialised HTTPS origin (scheme + host + +sites. Each entry must be a valid serialized HTTPS origin (scheme + host + optional port, no path). Mixing ``'*'`` with explicit origins is a **link-time error**. diff --git a/src/settings.js b/src/settings.js index b16facc67cafb..9a143397b9e5a 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2264,7 +2264,7 @@ var CROSS_ORIGIN_STORAGE = 0; // -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] // // For proprietary resources shared across a controlled set of related sites. -// Each value must be a valid serialised HTTPS origin (scheme + host + optional +// Each value must be a valid serialized HTTPS origin (scheme + host + optional // port, no path). Mixing ``'*'`` with explicit origins is a link-time error. // // **Same-site only** — pass the setting with an empty list to omit the From 9a06c2ca8e63e5868404171f6032aaa6eda7b5c0 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:17:51 +0200 Subject: [PATCH 27/87] docs: describe progressive enhancement and fallback path in COS example README --- test/cross_origin_storage/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md index 7e347edd8e434..c28f7f91c24af 100644 --- a/test/cross_origin_storage/README.md +++ b/test/cross_origin_storage/README.md @@ -7,11 +7,17 @@ into the standard Wasm loading path. ## What it does -On the **first load** the `.wasm` module is fetched over the network and -stored in the cross-origin cache, keyed by its SHA-256 hash. +COS is a **progressive enhancement**: when the browser exposes the +`navigator.crossOriginStorage` API, loading takes one of two paths: -On **subsequent loads** — from the same origin or any other — the module is -retrieved from the cache without a network request for the binary. +- **Cache miss** (first load): the `.wasm` module is fetched over the network + and stored in the cross-origin cache, keyed by its SHA-256 hash. +- **Cache hit** (subsequent loads, same or any other origin): the module is + retrieved from the cache without a network request for the binary. + +When the browser does not expose the COS API, or when an unexpected error +occurs, the runtime falls back to the standard `fetch()` / +`WebAssembly.instantiateStreaming()` path — the page always loads. The page reports which path was taken and, where applicable, the SHA-256 hash of the Wasm resource and the URL it was fetched from. From f24565d12857ef0b10b81515abbb394977b160a3 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 12:57:16 +0200 Subject: [PATCH 28/87] fix: ruff docstring formatting and regenerate settings_reference.rst --- .../tools_reference/settings_reference.rst | 80 +++++++++++++++++++ test/test_other.py | 26 +++--- 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index f26b0aa33b9a9..5138b0c74f935 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3360,6 +3360,86 @@ indirectly using `importScripts` Default value: false +.. _cross_origin_storage: + +CROSS_ORIGIN_STORAGE +==================== + +[experimental] Enables Cross-Origin Storage (COS) API support for Wasm +loading on the Web target. + +**When to use this flag** + +COS is only beneficial for Wasm binaries that are byte-identical across +many different origins — i.e. popular libraries where the same compiled +binary is loaded by many independent sites. +Good candidates are libraries or toolkits distributed as a stable, +version-pinned binary loaded by many independent sites. + +If your ``.wasm`` file is bespoke application code built specifically for +your site, COS gives you nothing that the normal HTTP cache does not +already provide. Do not enable this flag for application-specific Wasm. + +When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` +binary at link time, embeds it as a build-time constant in the generated +JavaScript glue, and uses the browser COS API as a progressive enhancement: + +- **Cache hit**: the runtime calls + ``navigator.crossOriginStorage.requestFileHandles()`` with the hash and, + if the module is found, reads it directly from the cross-origin cache. +- **Cache miss**: the module is fetched over the network as usual, then + stored in COS in the background (non-blocking) with ``origins: '*'`` so + any other origin can reuse it. +- **Fallback**: when the browser does not expose the COS API, or when an + unexpected error occurs, the runtime falls through to the standard + ``fetch`` / ``WebAssembly.instantiateStreaming`` path transparently. + +Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no +effect on Node.js or shell targets. Also has no effect in SINGLE_FILE +builds where the Wasm binary is inlined directly into the JS output. + +See :ref:`CrossOriginStorage` for the full guide, including how to test +with the COS browser extension polyfill. + +Default value: 0 + +.. _cross_origin_storage_origins: + +CROSS_ORIGIN_STORAGE_ORIGINS +============================ + +Controls which origins may read the Wasm binary after it has been stored in +the Cross-Origin Storage (COS) cache. Only meaningful when +``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) +path; it is only applied during the write (cache-miss) path. + +Three modes are supported, matching the ``origins`` field of the COS API's +``CrossOriginStorageRequestFileHandleOptions`` dictionary: + +**Globally available** (default when the setting is not explicitly passed) — +any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is +used without specifying this setting, ``origins: '*'`` is used automatically. +Appropriate for widely-used public binaries shared across many independent +origins. + +**Restricted to a specific set of origins** — only listed HTTPS origins can +retrieve the file:: + + -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] + +For proprietary resources shared across a controlled set of related sites. +Each value must be a valid serialized HTTPS origin (scheme + host + optional +port, no path). Mixing ``'*'`` with explicit origins is a link-time error. + +**Same-site only** — pass the setting with an empty list to omit the +``origins`` field, making the file available only to same-site origins:: + + -sCROSS_ORIGIN_STORAGE_ORIGINS=[] + +For resources shared across subdomains of a single site but not beyond. + +Default value: [] + .. _fake_dylibs: FAKE_DYLIBS diff --git a/test/test_other.py b/test/test_other.py index 1ff9adcb1a285..1ebebd39a3fcf 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15584,8 +15584,10 @@ def test_deprecated_settings(self): # --------------------------------------------------------------------------- def test_cross_origin_storage_js_output(self): - """COS code is present in JS when the feature is enabled for the web target, - and the embedded hash is the correct SHA-256 of the compiled .wasm file.""" + """COS code is present in JS when the feature is enabled for the web target. + + The embedded hash must be the correct SHA-256 of the compiled .wasm file. + """ self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', @@ -15636,9 +15638,11 @@ def test_cross_origin_storage_disabled_by_default(self): self.assertNotContained('crossOriginStorage', js) def test_cross_origin_storage_not_emitted_for_node_target(self): - """COS code must NOT appear when targeting Node.js only, even with the flag - set; the #if ENVIRONMENT_MAY_BE_WEB guard strips it. A warning must also - be emitted since the flag does nothing in this configuration.""" + """COS code must NOT appear when targeting Node.js only, even with the flag set. + + The #if ENVIRONMENT_MAY_BE_WEB guard strips it. A warning must also be + emitted since the flag does nothing in this configuration. + """ proc = self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=node', @@ -15724,8 +15728,10 @@ def test_cross_origin_storage_hash_changes_with_content(self): # --------------------------------------------------------------------------- def test_cross_origin_storage_origins_default_is_global(self): - """Without -sCROSS_ORIGIN_STORAGE_ORIGINS, the default must be origins:'*' - (globally available). The user only needs -sCROSS_ORIGIN_STORAGE=1.""" + """Without -sCROSS_ORIGIN_STORAGE_ORIGINS, the default must be origins:'*'. + + Globally available; the user only needs -sCROSS_ORIGIN_STORAGE=1. + """ self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', @@ -15733,8 +15739,10 @@ def test_cross_origin_storage_origins_default_is_global(self): self.assertContained("origins: '*'", read_file('hello.js')) def test_cross_origin_storage_origins_explicit_wildcard(self): - """Explicitly passing -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] must also emit - origins:'*', matching the implicit default.""" + """Explicitly passing -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] must emit origins:'*'. + + This matches the implicit default. + """ self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', From 4418bbe65d7cdfb6acb61502b590f629e9128a47 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 13:26:19 +0200 Subject: [PATCH 29/87] docs: warn about post-emcc wasm post-processing invalidating the embedded hash --- .../docs/compiling/CrossOriginStorage.rst | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 947385e6f734b..e8ba9337c4062 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -162,6 +162,28 @@ build-time constant:: No extra files are produced; the hash is part of the regular ``.js`` output. +.. warning:: + The hash is computed over the ``.wasm`` binary **as emcc produces it**, + after emcc's own internal Binaryen/``wasm-opt`` pass. If your build + pipeline runs additional wasm post-processing tools *after* emcc exits — + for example, an external ``wasm-strip`` or ``wasm-opt`` invocation in a + Makefile or CI script — those tools change the binary and **invalidate the + embedded hash**. + + In that case you must recompute the SHA-256 of the final ``.wasm`` and + patch the hash string in the generated ``.js`` yourself before shipping. + A minimal shell snippet for doing so: + + .. code-block:: bash + + # After all post-processing is complete: + final_hash=$(sha256sum hello.wasm | awk '{print $1}') + sed -i "s/value: '[0-9a-f]\{64\}'/value: '${final_hash}'/" hello.js + + On macOS, use ``shasum -a 256`` in place of ``sha256sum``, and install + GNU sed (``brew install gnu-sed``) or adapt the ``sed`` command for BSD + sed syntax. + Runtime (web only) ------------------ From c74dff6b4de94938cdb39e2fa319974086cf3a44 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 13:49:00 +0200 Subject: [PATCH 30/87] feat(cross-origin-storage): expose Module['wasmSHA256'] for custom instantiateWasm loaders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a program supplies its own Module['instantiateWasm'] callback, Emscripten calls it directly and skips instantiateAsync(), so the built-in COS fetch logic is never reached. To give custom loaders the information they need to implement their own COS-aware path, expose the build-time SHA-256 as Module['wasmSHA256'] (set before instantiateWasm is called) whenever -sCROSS_ORIGIN_STORAGE=1 is set. - src/preamble.js: assign Module['wasmSHA256'] from the WASM_SHA256 template literal, guarded by #if CROSS_ORIGIN_STORAGE, before the Module['instantiateWasm'] dispatch. - test/test_other.py: two new tests — one that checks the property is present and matches the .wasm SHA-256, one that checks it is absent without the flag. - site/source/docs/compiling/CrossOriginStorage.rst: new section "Custom Module['instantiateWasm'] implementations" documenting the bypass limitation and the Module['wasmSHA256'] escape hatch with a full worked example. --- .../docs/compiling/CrossOriginStorage.rst | 61 +++++++++++++++++++ src/preamble.js | 10 +++ test/test_other.py | 32 ++++++++++ 3 files changed, 103 insertions(+) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index e8ba9337c4062..1bef546854a1b 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -270,6 +270,67 @@ Both values must be identical. The Emscripten test suite checks this automatically via ``test_cross_origin_storage_js_output`` in ``test/test_other.py``. +Custom ``Module['instantiateWasm']`` implementations +===================================================== + +The COS fetch logic described above lives inside ``instantiateAsync()``, which +is the standard Emscripten wasm loading path. When a program provides its own +``Module['instantiateWasm']`` callback, Emscripten calls that callback directly +and **skips** ``instantiateAsync()`` entirely, so the built-in COS code is never +reached. + +To support COS in a custom loader, Emscripten exposes the build-time SHA-256 +hash as a named Module property: + +.. code-block:: javascript + + Module['wasmSHA256'] // 64-character lowercase hex string, e.g. 'a3f2…c9d1' + +This property is set by the generated JavaScript before +``Module['instantiateWasm']`` is called, so it is always available inside the +callback. A custom loader can use it to implement the same cache-hit / +cache-miss / store / fallback logic as the built-in path: + +.. code-block:: javascript + + var Module = { + instantiateWasm(imports, onSuccess) { + const cosHash = { algorithm: 'SHA-256', value: Module['wasmSHA256'] }; + if (cosHash.value && 'crossOriginStorage' in navigator) { + navigator.crossOriginStorage.requestFileHandles([cosHash]) + .then(handles => handles[0].getFile()) + .then(f => f.arrayBuffer()) + .then(bytes => WebAssembly.instantiate(bytes, imports)) + .then(({instance, module}) => onSuccess(instance, module)) + .catch(err => { + if (err.name !== 'NotFoundError') throw err; + // cache miss — fetch normally and store in the background + fetch('hello.wasm') + .then(r => r.arrayBuffer()) + .then(bytes => { + WebAssembly.instantiate(bytes, imports) + .then(({instance, module}) => onSuccess(instance, module)); + // fire-and-forget store + navigator.crossOriginStorage + .requestFileHandles([cosHash], { create: true, origins: '*' }) + .then(wh => wh[0].createWritable()) + .then(w => w.write(new Blob([bytes], {type:'application/wasm'})) + .then(() => w.close())); + }); + }); + return; // async; onSuccess called above + } + // fallback — normal streaming instantiation + WebAssembly.instantiateStreaming(fetch('hello.wasm'), imports) + .then(({instance, module}) => onSuccess(instance, module)); + }, + }; + +``Module['wasmSHA256']`` is only present in builds compiled with +``-sCROSS_ORIGIN_STORAGE=1``. Always guard on its truthiness before using it, +as shown above, so the same loader code works in builds compiled without the +flag. + Relationship to other caching mechanisms ========================================== diff --git a/src/preamble.js b/src/preamble.js index 78eea268d4fda..f48b6a9321981 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -916,6 +916,16 @@ function getWasmImports() { var info = getWasmImports(); +#if CROSS_ORIGIN_STORAGE + // Expose the build-time SHA-256 hash of the .wasm binary as a named Module + // property so that custom Module['instantiateWasm'] implementations can read + // it without having to parse the JS source. The COS fetch logic lives inside + // instantiateAsync(), which is bypassed when a custom instantiateWasm is + // provided. Such loaders can use Module['wasmSHA256'] to implement their own + // COS-aware loading path. + Module['wasmSHA256'] = '{{{ WASM_SHA256 }}}'; +#endif + #if expectToReceiveOnModule('instantiateWasm') // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback // to manually instantiate the Wasm module themselves. This allows pages to diff --git a/test/test_other.py b/test/test_other.py index 1ebebd39a3fcf..323b014bb1d01 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15803,3 +15803,35 @@ def test_cross_origin_storage_origins_error_origin_with_path(self): '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]', '-o', 'hello.js'], 'is not a valid HTTPS origin') + + def test_cross_origin_storage_wasm_sha256_module_property(self): + """Module['wasmSHA256'] must be set in the JS output and match the .wasm hash. + + Custom Module['instantiateWasm'] implementations bypass instantiateAsync() + and therefore cannot reach the COS fetch logic that lives there. They can + read Module['wasmSHA256'] instead to get the build-time hash without parsing + the JS source. + """ + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js = read_file('hello.js') + + # The property must be present with a 64-char hex value. + m = re.search(r"Module\['wasmSHA256'\]\s*=\s*'([0-9a-f]{64})'", js) + self.assertTrue(m, "Module['wasmSHA256'] not found in JS output") + embedded_hash = m.group(1) + + # It must equal the SHA-256 of the actual .wasm file. + expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() + self.assertEqual(embedded_hash, expected_hash, + "Module['wasmSHA256'] does not match the actual .wasm SHA-256") + + def test_cross_origin_storage_wasm_sha256_absent_without_flag(self): + """Module['wasmSHA256'] must NOT appear when CROSS_ORIGIN_STORAGE is off.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertNotContained("Module['wasmSHA256']", js) From 05be9799bfb0b1da19c9bb832aff023f4a91dc8a Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 8 Jun 2026 14:06:10 +0200 Subject: [PATCH 31/87] fix(cross-origin-storage): also patch Module['wasmSHA256'] in post-binaryen fixup; clarify Module reference in docs - tools/link.py: the post-binaryen hash patch was already updating value: '...' in the cosHash object but missed the new Module['wasmSHA256'] = '...' assignment added to preamble.js. Patch both so the exposed property stays accurate after binaryen modifies the wasm binary. - site/source/docs/compiling/CrossOriginStorage.rst: clarify that Module['wasmSHA256'] is set on the config object (Module = moduleArg in the generated JS), so custom loaders that use an intermediate module wrapper should read it via the outer config reference rather than via `this` inside the callback. --- site/source/docs/compiling/CrossOriginStorage.rst | 8 ++++++-- tools/link.py | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 1bef546854a1b..2db296a2fb32c 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -288,13 +288,17 @@ hash as a named Module property: This property is set by the generated JavaScript before ``Module['instantiateWasm']`` is called, so it is always available inside the -callback. A custom loader can use it to implement the same cache-hit / -cache-miss / store / fallback logic as the built-in path: +callback. ``Module`` in this context is the config object passed to the module +factory — whatever variable you use when calling ``new Module(config)`` or the +equivalent factory function. A custom loader can read ``Module['wasmSHA256']`` +via a reference to that config object: .. code-block:: javascript var Module = { instantiateWasm(imports, onSuccess) { + // `this` inside the callback is Emscripten's internal Module object; + // read the hash via the outer Module reference instead. const cosHash = { algorithm: 'SHA-256', value: Module['wasmSHA256'] }; if (cosHash.value && 'crossOriginStorage' in navigator) { navigator.crossOriginStorage.requestFileHandles([cosHash]) diff --git a/tools/link.py b/tools/link.py index 3726afde77dcb..92ad711cc110c 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1961,6 +1961,8 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat js_content = read_file(final_js) js_content = js_content.replace(f"value: '{settings.WASM_SHA256}'", f"value: '{post_hash}'") + js_content = js_content.replace(f"Module['wasmSHA256'] = '{settings.WASM_SHA256}'", + f"Module['wasmSHA256'] = '{post_hash}'") write_file(final_js, js_content) settings.WASM_SHA256 = post_hash From 5784e32998a870c2e79d9a4d5cb69875db4f11fb Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 11:47:07 +0200 Subject: [PATCH 32/87] docs: drop =1 from -sCROSS_ORIGIN_STORAGE flag references in COS guide --- .../docs/compiling/CrossOriginStorage.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 2db296a2fb32c..f0430566d162f 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -21,7 +21,7 @@ identified by their cryptographic hashes. A file stored in COS by one site can be retrieved by any other site using the same hash, eliminating redundant downloads. -Emscripten's ``-sCROSS_ORIGIN_STORAGE=1`` flag integrates this into the +Emscripten's ``-sCROSS_ORIGIN_STORAGE`` flag integrates this into the standard Wasm loading path. At build time, Emscripten computes the SHA-256 hash of the final ``.wasm`` binary. At runtime, the generated JavaScript tries to retrieve the compiled Wasm module from COS before falling back to @@ -59,9 +59,9 @@ the world. Usage ===== -Pass ``-sCROSS_ORIGIN_STORAGE=1`` at link time:: +Pass ``-sCROSS_ORIGIN_STORAGE`` at link time:: - emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 + emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE The flag is a **link-time** setting and has no effect during compilation of individual object files. @@ -75,12 +75,12 @@ effect on the read (cache-hit) path. Three modes are available: **Globally available** (default, no explicit setting needed) — any origin can retrieve the file. This is applied automatically when -``-sCROSS_ORIGIN_STORAGE=1`` is used without specifying +``-sCROSS_ORIGIN_STORAGE`` is used without specifying ``-sCROSS_ORIGIN_STORAGE_ORIGINS``: .. code-block:: bash - emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE=1 + emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE Use this for popular binaries loaded by many independent origins. This is the recommended mode for resources where global COS cache hits are expected. @@ -91,7 +91,7 @@ retrieve the file: .. code-block:: bash emcc hello.cpp -o hello.js \ - -sCROSS_ORIGIN_STORAGE=1 \ + -sCROSS_ORIGIN_STORAGE \ '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://app.example.com","https://api.example.com"]' Use this for proprietary resources shared across a controlled set of related @@ -105,7 +105,7 @@ field, making the file available only to same-site origins: .. code-block:: bash emcc hello.cpp -o hello.js \ - -sCROSS_ORIGIN_STORAGE=1 \ + -sCROSS_ORIGIN_STORAGE \ -sCROSS_ORIGIN_STORAGE_ORIGINS=[] Use this for resources that should be shared across subdomains of a single @@ -246,7 +246,7 @@ the `Cross-Origin Storage extension which injects a ``navigator.crossOriginStorage`` polyfill on every page. 1. Install the extension in Chrome. -2. Build your project with ``-sCROSS_ORIGIN_STORAGE=1 -sENVIRONMENT=web``. +2. Build your project with ``-sCROSS_ORIGIN_STORAGE -sENVIRONMENT=web``. 3. Serve the output over HTTP (e.g. with ``emrun`` or ``python3 -m http.server``). 4. Open the page — on the first load the Wasm binary is fetched and stored in COS. Open the same page in a second tab or from a different origin: the @@ -331,7 +331,7 @@ via a reference to that config object: }; ``Module['wasmSHA256']`` is only present in builds compiled with -``-sCROSS_ORIGIN_STORAGE=1``. Always guard on its truthiness before using it, +``-sCROSS_ORIGIN_STORAGE``. Always guard on its truthiness before using it, as shown above, so the same loader code works in builds compiled without the flag. From 2e42daca694b4a37fdd8c2fad39ddf8c47a7897f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 11:52:37 +0200 Subject: [PATCH 33/87] docs: use :ref: cross-references for settings in COS guide prose --- .../source/docs/compiling/CrossOriginStorage.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index f0430566d162f..6b3897cbe6030 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -21,7 +21,7 @@ identified by their cryptographic hashes. A file stored in COS by one site can be retrieved by any other site using the same hash, eliminating redundant downloads. -Emscripten's ``-sCROSS_ORIGIN_STORAGE`` flag integrates this into the +Emscripten's :ref:`CROSS_ORIGIN_STORAGE` flag integrates this into the standard Wasm loading path. At build time, Emscripten computes the SHA-256 hash of the final ``.wasm`` binary. At runtime, the generated JavaScript tries to retrieve the compiled Wasm module from COS before falling back to @@ -52,14 +52,14 @@ already handles per-origin caching efficiently. The exception is a Wasm binary that you deploy across **multiple origins you own** — for example, the same library shared between ``https://app.example.com`` and ``https://api.example.com``. In that case COS can eliminate the redundant -download between your own origins. Use ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` to +download between your own origins. Use :ref:`CROSS_ORIGIN_STORAGE_ORIGINS` to restrict access to only those origins rather than opening the cache entry to the world. Usage ===== -Pass ``-sCROSS_ORIGIN_STORAGE`` at link time:: +Pass :ref:`CROSS_ORIGIN_STORAGE` at link time:: emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE @@ -69,14 +69,14 @@ individual object files. Controlling which origins can read the cached file -------------------------------------------------- -The ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` setting controls the ``origins`` field +The :ref:`CROSS_ORIGIN_STORAGE_ORIGINS` setting controls the ``origins`` field passed to ``requestFileHandles()`` on the write (cache-miss) path. It has no effect on the read (cache-hit) path. Three modes are available: **Globally available** (default, no explicit setting needed) — any origin can retrieve the file. This is applied automatically when -``-sCROSS_ORIGIN_STORAGE`` is used without specifying -``-sCROSS_ORIGIN_STORAGE_ORIGINS``: +:ref:`CROSS_ORIGIN_STORAGE` is used without specifying +:ref:`CROSS_ORIGIN_STORAGE_ORIGINS`: .. code-block:: bash @@ -204,7 +204,7 @@ When the page loads, the generated JavaScript follows this logic: defined, call ``WebAssembly.instantiate()`` immediately so the page loads without delay, and then write the bytes into COS in the background (fire-and-forget) using the ``origins`` value controlled by - ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` (``'*'`` by default). + :ref:`CROSS_ORIGIN_STORAGE_ORIGINS` (``'*'`` by default). Once the write completes, invoke ``Module['onCOSStore'](hash)`` if defined. 4. **Fallback** — any unexpected error (``NotAllowedError`` from the browser, @@ -331,7 +331,7 @@ via a reference to that config object: }; ``Module['wasmSHA256']`` is only present in builds compiled with -``-sCROSS_ORIGIN_STORAGE``. Always guard on its truthiness before using it, +:ref:`CROSS_ORIGIN_STORAGE`. Always guard on its truthiness before using it, as shown above, so the same loader code works in builds compiled without the flag. From 88c40111b676ec66368012dc1716818b6f3a95f8 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 11:54:37 +0200 Subject: [PATCH 34/87] docs: remove redundant link-time note from COS usage section --- site/source/docs/compiling/CrossOriginStorage.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 6b3897cbe6030..dd7057ca98384 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -63,9 +63,6 @@ Pass :ref:`CROSS_ORIGIN_STORAGE` at link time:: emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE -The flag is a **link-time** setting and has no effect during compilation of -individual object files. - Controlling which origins can read the cached file -------------------------------------------------- From cd558c49e1ca459add993217375f1f09ffbb27b5 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 11:56:06 +0200 Subject: [PATCH 35/87] docs: use unquoted comma-separated syntax for CROSS_ORIGIN_STORAGE_ORIGINS example --- site/source/docs/compiling/CrossOriginStorage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index dd7057ca98384..ce3666eb12fa3 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -89,7 +89,7 @@ retrieve the file: emcc hello.cpp -o hello.js \ -sCROSS_ORIGIN_STORAGE \ - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://app.example.com","https://api.example.com"]' + -sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com Use this for proprietary resources shared across a controlled set of related sites. Each entry must be a valid serialized HTTPS origin (scheme + host + From 07c3746dfa0a2679ae0825e0866b5a469dcde766 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:00:50 +0200 Subject: [PATCH 36/87] docs: use comma-separated syntax for CROSS_ORIGIN_STORAGE_ORIGINS examples --- src/settings.js | 2 +- test/test_other.py | 2 +- tools/link.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/settings.js b/src/settings.js index 9a143397b9e5a..72ec688df37a3 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2261,7 +2261,7 @@ var CROSS_ORIGIN_STORAGE = 0; // **Restricted to a specific set of origins** — only listed HTTPS origins can // retrieve the file:: // -// -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] +// -sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com // // For proprietary resources shared across a controlled set of related sites. // Each value must be a valid serialized HTTPS origin (scheme + host + optional diff --git a/test/test_other.py b/test/test_other.py index 323b014bb1d01..6982d40b44c60 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15755,7 +15755,7 @@ def test_cross_origin_storage_origins_explicit_list(self): self.run_process([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE=1', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://app.example.com","https://api.example.com"]', + '-sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com', '-o', 'hello.js']) js = read_file('hello.js') self.assertContained('"https://app.example.com"', js) diff --git a/tools/link.py b/tools/link.py index 92ad711cc110c..7cf0fa5a5bc06 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1231,7 +1231,7 @@ def limit_incoming_module_api(): settings.CROSS_ORIGIN_STORAGE_ORIGINS = ['*'] origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS if not isinstance(origins, list): - exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. ['*'] or ['https://example.com']") + exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. https://example.com or https://a.com,https://b.com") if '*' in origins and len(origins) > 1: exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") for o in origins: From 584e0fa17e83de03e6c0decc9216895a43c793c8 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:03:02 +0200 Subject: [PATCH 37/87] fix: make CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 a hard error --- .../docs/compiling/CrossOriginStorage.rst | 6 +++--- test/test_other.py | 18 ++++++++---------- tools/link.py | 2 +- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index ce3666eb12fa3..607cc2589d6c9 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -132,9 +132,9 @@ Requirements and restrictions (``-sSINGLE_FILE``): the Wasm binary is embedded directly into the JS output and has no standalone ``.wasm`` file or fetchable URL to key the hash on. -- It emits a **warning** with ``-sWASM_ASYNC_COMPILATION=0``: the - synchronous instantiation path bypasses ``instantiateAsync()`` entirely, - so the COS code is never reached. +- It produces a **hard link-time error** with ``-sWASM_ASYNC_COMPILATION=0``: + the synchronous instantiation path bypasses ``instantiateAsync()`` entirely, + so the COS code can never be reached. - It covers **only the primary ``.wasm`` file**. Secondary files produced by ``-sSPLIT_MODULE`` (``.deferred.wasm``) and side modules loaded at runtime via ``dlopen`` in ``-sMAIN_MODULE`` builds are fetched through the normal diff --git a/test/test_other.py b/test/test_other.py index 6982d40b44c60..965b7307796b1 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15661,16 +15661,14 @@ def test_cross_origin_storage_error_with_single_file(self): '-o', 'hello.js'], 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') - def test_cross_origin_storage_warning_without_async_compilation(self): - """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must warn.""" - proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', - '-sENVIRONMENT=web', - '-sWASM_ASYNC_COMPILATION=0', - '-o', 'hello.js'], - stderr=PIPE) - self.assertContained('CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0', - proc.stderr) + def test_cross_origin_storage_error_without_async_compilation(self): + """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must be a hard link-time error.""" + self.assert_fail([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE=1', + '-sENVIRONMENT=web', + '-sWASM_ASYNC_COMPILATION=0', + '-o', 'hello.js'], + 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') def test_cross_origin_storage_warning_with_split_module(self): """CROSS_ORIGIN_STORAGE + SPLIT_MODULE must warn that secondary files are not covered.""" diff --git a/tools/link.py b/tools/link.py index 7cf0fa5a5bc06..31e5a54f7ad30 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1215,7 +1215,7 @@ def limit_incoming_module_api(): if not settings.ENVIRONMENT_MAY_BE_WEB: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web (navigator.crossOriginStorage is not available outside the browser)') if not settings.WASM_ASYNC_COMPILATION: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') + exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') if settings.SPLIT_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') if settings.MAIN_MODULE: From ede7ad5b63f854f73d972d4ed6ae5affcf57e0bc Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:04:52 +0200 Subject: [PATCH 38/87] docs: update build-time constant example to match current cosHash object form --- site/source/docs/compiling/CrossOriginStorage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 607cc2589d6c9..3538da955de5e 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -155,7 +155,7 @@ After all optimizations — including any ``wasm-opt`` passes run by Binaryen digest. That digest is embedded in the generated JavaScript glue as a build-time constant:: - var wasmHashValue = 'a3f2...c9d1'; // 64 hex characters + var cosHash = { algorithm: 'SHA-256', value: 'a3f2...c9d1' }; // value: 64 hex characters No extra files are produced; the hash is part of the regular ``.js`` output. @@ -190,7 +190,7 @@ When the page loads, the generated JavaScript follows this logic: If the API is absent, skip to the normal fetch path immediately. 2. **Cache hit** — call - ``navigator.crossOriginStorage.requestFileHandles([{algorithm: 'SHA-256', value: wasmHashValue}])``. + ``navigator.crossOriginStorage.requestFileHandles([cosHash])``. If the handle is returned (the module is already in COS), read it with ``handle.getFile()`` → ``.arrayBuffer()`` and pass the bytes to ``WebAssembly.instantiate()``. From d45c8d52ac20119f8a78a8bcf268f6410d401343 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:09:10 +0200 Subject: [PATCH 39/87] docs: use Module['wasmSHA256'] consistently and fix sed to patch both hash occurrences --- site/source/docs/compiling/CrossOriginStorage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 3538da955de5e..3691fa20e8f79 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -155,7 +155,7 @@ After all optimizations — including any ``wasm-opt`` passes run by Binaryen digest. That digest is embedded in the generated JavaScript glue as a build-time constant:: - var cosHash = { algorithm: 'SHA-256', value: 'a3f2...c9d1' }; // value: 64 hex characters + Module['wasmSHA256'] = 'a3f2...c9d1'; // 64 hex characters No extra files are produced; the hash is part of the regular ``.js`` output. @@ -175,7 +175,7 @@ No extra files are produced; the hash is part of the regular ``.js`` output. # After all post-processing is complete: final_hash=$(sha256sum hello.wasm | awk '{print $1}') - sed -i "s/value: '[0-9a-f]\{64\}'/value: '${final_hash}'/" hello.js + sed -i "s/'[0-9a-f]\{64\}'/'${final_hash}'/g" hello.js On macOS, use ``shasum -a 256`` in place of ``sha256sum``, and install GNU sed (``brew install gnu-sed``) or adapt the ``sed`` command for BSD From 991f2c8830681b629be74c64f886bd79b63b40c1 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:11:23 +0200 Subject: [PATCH 40/87] refactor: rename wasmSHA256 to wasmHash and expose {algorithm,value} object --- site/source/docs/compiling/CrossOriginStorage.rst | 12 ++++++------ src/preamble.js | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 3691fa20e8f79..d9a09f8d657d1 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -155,7 +155,7 @@ After all optimizations — including any ``wasm-opt`` passes run by Binaryen digest. That digest is embedded in the generated JavaScript glue as a build-time constant:: - Module['wasmSHA256'] = 'a3f2...c9d1'; // 64 hex characters + Module['wasmHash'] = { algorithm: 'SHA-256', value: 'a3f2...c9d1' }; No extra files are produced; the hash is part of the regular ``.js`` output. @@ -281,13 +281,13 @@ hash as a named Module property: .. code-block:: javascript - Module['wasmSHA256'] // 64-character lowercase hex string, e.g. 'a3f2…c9d1' + Module['wasmHash'] // { algorithm: 'SHA-256', value: '<64 hex chars>' } This property is set by the generated JavaScript before ``Module['instantiateWasm']`` is called, so it is always available inside the callback. ``Module`` in this context is the config object passed to the module factory — whatever variable you use when calling ``new Module(config)`` or the -equivalent factory function. A custom loader can read ``Module['wasmSHA256']`` +equivalent factory function. A custom loader can read ``Module['wasmHash']`` via a reference to that config object: .. code-block:: javascript @@ -296,8 +296,8 @@ via a reference to that config object: instantiateWasm(imports, onSuccess) { // `this` inside the callback is Emscripten's internal Module object; // read the hash via the outer Module reference instead. - const cosHash = { algorithm: 'SHA-256', value: Module['wasmSHA256'] }; - if (cosHash.value && 'crossOriginStorage' in navigator) { + const cosHash = Module['wasmHash']; + if (cosHash?.value && 'crossOriginStorage' in navigator) { navigator.crossOriginStorage.requestFileHandles([cosHash]) .then(handles => handles[0].getFile()) .then(f => f.arrayBuffer()) @@ -327,7 +327,7 @@ via a reference to that config object: }, }; -``Module['wasmSHA256']`` is only present in builds compiled with +``Module['wasmHash']`` is only present in builds compiled with :ref:`CROSS_ORIGIN_STORAGE`. Always guard on its truthiness before using it, as shown above, so the same loader code works in builds compiled without the flag. diff --git a/src/preamble.js b/src/preamble.js index f48b6a9321981..0191eb734ec52 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -917,13 +917,13 @@ function getWasmImports() { var info = getWasmImports(); #if CROSS_ORIGIN_STORAGE - // Expose the build-time SHA-256 hash of the .wasm binary as a named Module - // property so that custom Module['instantiateWasm'] implementations can read - // it without having to parse the JS source. The COS fetch logic lives inside + // Expose the build-time hash of the .wasm binary as a named Module property + // so that custom Module['instantiateWasm'] implementations can read it + // without having to parse the JS source. The COS fetch logic lives inside // instantiateAsync(), which is bypassed when a custom instantiateWasm is - // provided. Such loaders can use Module['wasmSHA256'] to implement their own + // provided. Such loaders can use Module['wasmHash'] to implement their own // COS-aware loading path. - Module['wasmSHA256'] = '{{{ WASM_SHA256 }}}'; + Module['wasmHash'] = { algorithm: 'SHA-256', value: '{{{ WASM_SHA256 }}}' }; #endif #if expectToReceiveOnModule('instantiateWasm') From 7f5ea258aa4c3f9268861800a0a6cd81371bc2d7 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 12:55:47 +0200 Subject: [PATCH 41/87] refactor: address multiple PR review comments on CROSS_ORIGIN_STORAGE - Use INCOMPATIBLE_SETTINGS in settings.py for CROSS_ORIGIN_STORAGE + SINGLE_FILE and CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 pairs, removing manual error checks from link.py - Change default CROSS_ORIGIN_STORAGE_ORIGINS from [] (sentinel) to ['*'] in settings.js, removing the manual default override in link.py - Type-check of ORIGINS is now handled automatically by settings.py - Replace {{{ WASM_SHA256 }}} template substitution with algorithm-agnostic <<< WASM_HASH_ALGORITHM >>> / <<< WASM_HASH_VALUE >>> late replacements; hash is computed once post-binaryen, removing the pre-binaryen pass - Use utils.read_binary() instead of manual file open in link.py - cosHash in preamble.js now references Module['wasmHash'] directly instead of redeclaring the hash object, so the value appears only once in the JS - Trim long inline comment blocks in preamble.js and settings.js; point to the CrossOriginStorage guide for full details - Use optional chaining (?.) for Module['onCOSCacheHit/Miss/Store'] calls - Drop =1 suffix from all -sCROSS_ORIGIN_STORAGE references in ChangeLog, test_other.py comments, and docs - Rename test_cross_origin_storage_wasm_sha256_* tests to wasmHash variants --- ChangeLog.md | 8 +- .../docs/compiling/CrossOriginStorage.rst | 12 +-- .../tools_reference/settings_reference.rst | 76 +++++-------------- src/preamble.js | 59 +++----------- src/settings.js | 74 +++++------------- test/test_other.py | 67 ++++++++-------- tools/link.py | 55 ++++---------- tools/settings.py | 2 + 8 files changed, 107 insertions(+), 246 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 6901b943e7d3f..6d35d38420efc 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -36,6 +36,7 @@ See docs/process.md for more on how version tagging works. - Fixed `getentropy`/`random_get` spuriously failing under Node.js and the shell environment for small requests. (#27122) - New experimental ``-sCROSS_ORIGIN_STORAGE=1`` linker flag that integrates +- New experimental ``-sCROSS_ORIGIN_STORAGE`` linker flag that integrates the proposed `Cross-Origin Storage browser API `_ into the Wasm loading path as a progressive enhancement (web target only). At build time Emscripten @@ -46,10 +47,9 @@ See docs/process.md for more on how version tagging works. normally and stores the binary in COS for future use by any origin. Falls back transparently to the standard fetch path when the API is unavailable. Three optional ``Module`` callbacks are available for instrumentation: - ``Module['onCOSCacheHit'](hash)``, ``Module['onCOSCacheMiss'](url)``, and - ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` - (hard error — the wasm is inlined directly into the JS output with no - fetchable URL) and ``-sWASM_ASYNC_COMPILATION=0`` (warning). + ``Module['onCOSCacheHit'](hash)``, ``Module['onCOSCacheMiss'](hash, url)``, + and ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` + and ``-sWASM_ASYNC_COMPILATION=0`` (both produce hard link-time errors). The companion ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` setting controls which origins may read the cached file: ``['*']`` (default, globally available), an explicit HTTPS origin list (restricted), or ``[]`` (same-site only). diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index d9a09f8d657d1..28cf1d1018baf 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -151,9 +151,9 @@ Build time ---------- After all optimizations — including any ``wasm-opt`` passes run by Binaryen -— Emscripten reads the final ``.wasm`` binary and computes its SHA-256 -digest. That digest is embedded in the generated JavaScript glue as a -build-time constant:: +— Emscripten reads the final ``.wasm`` binary and hashes it. The hash +object is embedded in the generated JavaScript glue as a build-time +constant (currently SHA-256):: Module['wasmHash'] = { algorithm: 'SHA-256', value: 'a3f2...c9d1' }; @@ -167,9 +167,9 @@ No extra files are produced; the hash is part of the regular ``.js`` output. Makefile or CI script — those tools change the binary and **invalidate the embedded hash**. - In that case you must recompute the SHA-256 of the final ``.wasm`` and - patch the hash string in the generated ``.js`` yourself before shipping. - A minimal shell snippet for doing so: + In that case you must recompute the hash of the final ``.wasm`` and + patch the value string in the generated ``.js`` yourself before shipping. + A minimal shell snippet for doing so (SHA-256): .. code-block:: bash diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 5138b0c74f935..2940422148e77 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3366,40 +3366,17 @@ CROSS_ORIGIN_STORAGE ==================== [experimental] Enables Cross-Origin Storage (COS) API support for Wasm -loading on the Web target. +loading on the Web target. At link time Emscripten computes the SHA-256 +hash of the final ``.wasm`` binary and embeds it in the generated JS. +At runtime the COS API is used as a progressive enhancement: the binary is +fetched from the shared cross-origin cache on a hit, or stored there after +a network fetch on a miss; when the API is absent or errors the runtime +falls through to the standard fetch path. -**When to use this flag** +Only meaningful for the Web environment. Incompatible with SINGLE_FILE and +WASM_ASYNC_COMPILATION=0 (both produce hard link-time errors). -COS is only beneficial for Wasm binaries that are byte-identical across -many different origins — i.e. popular libraries where the same compiled -binary is loaded by many independent sites. -Good candidates are libraries or toolkits distributed as a stable, -version-pinned binary loaded by many independent sites. - -If your ``.wasm`` file is bespoke application code built specifically for -your site, COS gives you nothing that the normal HTTP cache does not -already provide. Do not enable this flag for application-specific Wasm. - -When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` -binary at link time, embeds it as a build-time constant in the generated -JavaScript glue, and uses the browser COS API as a progressive enhancement: - -- **Cache hit**: the runtime calls - ``navigator.crossOriginStorage.requestFileHandles()`` with the hash and, - if the module is found, reads it directly from the cross-origin cache. -- **Cache miss**: the module is fetched over the network as usual, then - stored in COS in the background (non-blocking) with ``origins: '*'`` so - any other origin can reuse it. -- **Fallback**: when the browser does not expose the COS API, or when an - unexpected error occurs, the runtime falls through to the standard - ``fetch`` / ``WebAssembly.instantiateStreaming`` path transparently. - -Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no -effect on Node.js or shell targets. Also has no effect in SINGLE_FILE -builds where the Wasm binary is inlined directly into the JS output. - -See :ref:`CrossOriginStorage` for the full guide, including how to test -with the COS browser extension polyfill. +See :ref:`CrossOriginStorage` for the full guide. Default value: 0 @@ -3408,37 +3385,20 @@ Default value: 0 CROSS_ORIGIN_STORAGE_ORIGINS ============================ -Controls which origins may read the Wasm binary after it has been stored in -the Cross-Origin Storage (COS) cache. Only meaningful when -``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) -path; it is only applied during the write (cache-miss) path. +Controls which origins may read the Wasm binary from the COS cache. Only +meaningful when ``-sCROSS_ORIGIN_STORAGE`` is set. Applied only during the +write (cache-miss) path, not the read (cache-hit) path. -Three modes are supported, matching the ``origins`` field of the COS API's -``CrossOriginStorageRequestFileHandleOptions`` dictionary: +``['*']`` (default) — any origin can retrieve the file. +Explicit HTTPS origin list — restricted to those origins only:: -**Globally available** (default when the setting is not explicitly passed) — -any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is -used without specifying this setting, ``origins: '*'`` is used automatically. -Appropriate for widely-used public binaries shared across many independent -origins. + -sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com -**Restricted to a specific set of origins** — only listed HTTPS origins can -retrieve the file:: +``[]`` — same-site only (omits the ``origins`` field entirely). - -sCROSS_ORIGIN_STORAGE_ORIGINS=['https://app.example.com','https://api.example.com'] +Mixing ``'*'`` with explicit origins is a link-time error. -For proprietary resources shared across a controlled set of related sites. -Each value must be a valid serialized HTTPS origin (scheme + host + optional -port, no path). Mixing ``'*'`` with explicit origins is a link-time error. - -**Same-site only** — pass the setting with an empty list to omit the -``origins`` field, making the file available only to same-site origins:: - - -sCROSS_ORIGIN_STORAGE_ORIGINS=[] - -For resources shared across subdomains of a single site but not beyond. - -Default value: [] +Default value: ['*'] .. _fake_dylibs: diff --git a/src/preamble.js b/src/preamble.js index 0191eb734ec52..92c303b862dac 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -630,46 +630,16 @@ async function instantiateAsync(binary, binaryFile, imports) { #if CROSS_ORIGIN_STORAGE // Cross-Origin Storage (COS) progressive enhancement. // https://github.com/WICG/cross-origin-storage - // - // COS is only beneficial when this .wasm binary is byte-identical across - // many origins — i.e. a popular library loaded by many independent sites. - // Application-specific Wasm gains nothing from COS that the normal HTTP - // cache does not already provide. - // - // The SHA-256 hash of the final .wasm binary is computed at link time and - // embedded here as a build-time constant. At runtime we feature-detect the - // browser COS API via `'crossOriginStorage' in navigator`, then call - // navigator.crossOriginStorage.requestFileHandles() with the hash object - // required by the spec ({ algorithm: 'SHA-256', value: '' }). - // - // Cache-hit path: - // requestFileHandles() succeeds → getFile() → arrayBuffer() → instantiate - // - // Cache-miss path (NotFoundError): - // fetch() the wasm over the network → instantiate → store in COS. - // The origins field is controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS - // (default: '*', globally available). The store is fire-and-forget so - // it never delays startup. - // - // Visibility and security: the spec allows widening visibility (e.g. a - // restricted entry promoted to globally available) but never narrowing it. - // Because storing always requires writing the actual bytes, no third party - // can probe the cache to discover whether a restricted entry was previously - // stored by another origin — a cache hit is only possible after an explicit - // write that provided the content. - // - // Any other error (NotAllowedError, network failure, …) falls through to the + // Any error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. - var cosHash = { algorithm: 'SHA-256', value: '{{{ WASM_SHA256 }}}' }; - if (cosHash.value && 'crossOriginStorage' in navigator) { + var cosHash = Module['wasmHash']; + if (cosHash?.value && 'crossOriginStorage' in navigator) { try { var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); // Cache hit — read the Blob and instantiate from its ArrayBuffer. var cosFile = await cosHandles[0].getFile(); var cosBytes = await cosFile.arrayBuffer(); - // Optional instrumentation callback: Module['onCOSCacheHit'](hash) - // Called when the Wasm binary is served from the cross-origin cache. - if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](cosHash.value); + Module['onCOSCacheHit']?.(cosHash.value); return WebAssembly.instantiate(cosBytes, imports); } catch (cosErr) { if (cosErr.name === 'NotFoundError') { @@ -677,9 +647,7 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); var wasmBytes = await networkResponse.arrayBuffer(); - // Optional instrumentation callback: Module['onCOSCacheMiss'](hash, url) - // Called when the Wasm binary is not in COS and is fetched over the network. - if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](cosHash.value, binaryFile); + Module['onCOSCacheMiss']?.(cosHash.value, binaryFile); // Fire-and-forget store; never block instantiation on the write. (async () => { try { @@ -696,16 +664,14 @@ async function instantiateAsync(binary, binaryFile, imports) { var writable = await writeHandles[0].createWritable(); await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); await writable.close(); - // Optional instrumentation callback: Module['onCOSStore'](hash) - // Called after the Wasm binary has been successfully written to COS. - if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](cosHash.value); + Module['onCOSStore']?.(cosHash.value); } catch (storeErr) { err(`COS store failed: ${storeErr}`); } })(); return WebAssembly.instantiate(wasmBytes, imports); } catch (fetchErr) { - // Network fetch failed; fall through to the standard path. + // Network fetch failed; fall through to the standard path below. err(`COS fallback fetch failed: ${fetchErr}`); } } else if (cosErr.name === 'NotAllowedError') { @@ -713,6 +679,7 @@ async function instantiateAsync(binary, binaryFile, imports) { } else { err(`Cross-Origin Storage lookup failed: ${cosErr}`); } + // Fall through to the standard streaming path below. } } #endif // CROSS_ORIGIN_STORAGE @@ -917,13 +884,9 @@ function getWasmImports() { var info = getWasmImports(); #if CROSS_ORIGIN_STORAGE - // Expose the build-time hash of the .wasm binary as a named Module property - // so that custom Module['instantiateWasm'] implementations can read it - // without having to parse the JS source. The COS fetch logic lives inside - // instantiateAsync(), which is bypassed when a custom instantiateWasm is - // provided. Such loaders can use Module['wasmHash'] to implement their own - // COS-aware loading path. - Module['wasmHash'] = { algorithm: 'SHA-256', value: '{{{ WASM_SHA256 }}}' }; + // Expose the build-time hash so that custom Module['instantiateWasm'] + // callbacks can implement their own COS-aware loading path. + Module['wasmHash'] = { algorithm: '<<< WASM_HASH_ALGORITHM >>>', value: '<<< WASM_HASH_VALUE >>>' }; #endif #if expectToReceiveOnModule('instantiateWasm') diff --git a/src/settings.js b/src/settings.js index 72ec688df37a3..77e3e6044c6ba 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2206,75 +2206,35 @@ var GROWABLE_ARRAYBUFFERS = false; var CROSS_ORIGIN = false; // [experimental] Enables Cross-Origin Storage (COS) API support for Wasm -// loading on the Web target. +// loading on the Web target. At link time Emscripten computes the SHA-256 +// hash of the final ``.wasm`` binary and embeds it in the generated JS. +// At runtime the COS API is used as a progressive enhancement: the binary is +// fetched from the shared cross-origin cache on a hit, or stored there after +// a network fetch on a miss; when the API is absent or errors the runtime +// falls through to the standard fetch path. // -// **When to use this flag** +// Only meaningful for the Web environment. Incompatible with SINGLE_FILE and +// WASM_ASYNC_COMPILATION=0 (both produce hard link-time errors). // -// COS is only beneficial for Wasm binaries that are byte-identical across -// many different origins — i.e. popular libraries where the same compiled -// binary is loaded by many independent sites. -// Good candidates are libraries or toolkits distributed as a stable, -// version-pinned binary loaded by many independent sites. -// -// If your ``.wasm`` file is bespoke application code built specifically for -// your site, COS gives you nothing that the normal HTTP cache does not -// already provide. Do not enable this flag for application-specific Wasm. -// -// When enabled, Emscripten computes the SHA-256 hash of the final ``.wasm`` -// binary at link time, embeds it as a build-time constant in the generated -// JavaScript glue, and uses the browser COS API as a progressive enhancement: -// -// - **Cache hit**: the runtime calls -// ``navigator.crossOriginStorage.requestFileHandles()`` with the hash and, -// if the module is found, reads it directly from the cross-origin cache. -// - **Cache miss**: the module is fetched over the network as usual, then -// stored in COS in the background (non-blocking) with ``origins: '*'`` so -// any other origin can reuse it. -// - **Fallback**: when the browser does not expose the COS API, or when an -// unexpected error occurs, the runtime falls through to the standard -// ``fetch`` / ``WebAssembly.instantiateStreaming`` path transparently. -// -// Only meaningful for the Web environment (``-sENVIRONMENT=web``); has no -// effect on Node.js or shell targets. Also has no effect in SINGLE_FILE -// builds where the Wasm binary is inlined directly into the JS output. -// -// See :ref:`CrossOriginStorage` for the full guide, including how to test -// with the COS browser extension polyfill. +// See :ref:`CrossOriginStorage` for the full guide. // // [link] var CROSS_ORIGIN_STORAGE = 0; -// Controls which origins may read the Wasm binary after it has been stored in -// the Cross-Origin Storage (COS) cache. Only meaningful when -// ``-sCROSS_ORIGIN_STORAGE=1`` is set. Has no effect on the read (cache-hit) -// path; it is only applied during the write (cache-miss) path. +// Controls which origins may read the Wasm binary from the COS cache. Only +// meaningful when ``-sCROSS_ORIGIN_STORAGE`` is set. Applied only during the +// write (cache-miss) path, not the read (cache-hit) path. // -// Three modes are supported, matching the ``origins`` field of the COS API's -// ``CrossOriginStorageRequestFileHandleOptions`` dictionary: -// -// **Globally available** (default when the setting is not explicitly passed) — -// any origin can retrieve the file. When ``-sCROSS_ORIGIN_STORAGE=1`` is -// used without specifying this setting, ``origins: '*'`` is used automatically. -// Appropriate for widely-used public binaries shared across many independent -// origins. -// -// **Restricted to a specific set of origins** — only listed HTTPS origins can -// retrieve the file:: +// ``['*']`` (default) — any origin can retrieve the file. +// Explicit HTTPS origin list — restricted to those origins only:: // // -sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com // -// For proprietary resources shared across a controlled set of related sites. -// Each value must be a valid serialized HTTPS origin (scheme + host + optional -// port, no path). Mixing ``'*'`` with explicit origins is a link-time error. -// -// **Same-site only** — pass the setting with an empty list to omit the -// ``origins`` field, making the file available only to same-site origins:: -// -// -sCROSS_ORIGIN_STORAGE_ORIGINS=[] +// ``[]`` — same-site only (omits the ``origins`` field entirely). // -// For resources shared across subdomains of a single site but not beyond. +// Mixing ``'*'`` with explicit origins is a link-time error. // [link] -var CROSS_ORIGIN_STORAGE_ORIGINS = []; +var CROSS_ORIGIN_STORAGE_ORIGINS = ['*']; // This setting changes the behaviour of the ``-shared`` flag. When set to true // you get the old emscripten behaviour where the ``-shared`` flag actually diff --git a/test/test_other.py b/test/test_other.py index 965b7307796b1..5b86b72311571 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15579,7 +15579,7 @@ def test_deprecated_settings(self): # --------------------------------------------------------------------------- - # Tests for CROSS_ORIGIN_STORAGE (-sCROSS_ORIGIN_STORAGE=1) + # Tests for CROSS_ORIGIN_STORAGE (-sCROSS_ORIGIN_STORAGE) # https://github.com/WICG/cross-origin-storage # --------------------------------------------------------------------------- @@ -15589,7 +15589,7 @@ def test_cross_origin_storage_js_output(self): The embedded hash must be the correct SHA-256 of the compiled .wasm file. """ self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') @@ -15621,13 +15621,13 @@ def test_cross_origin_storage_js_output(self): # The hash embedded in the JS must be a 64-char lowercase hex string … m = re.search(r"value:\s*'([0-9a-f]{64})'", js) - self.assertTrue(m, 'could not find a 64-char hex WASM_SHA256 value in JS output') + self.assertTrue(m, 'could not find a 64-char hex hash value in JS output') embedded_hash = m.group(1) # … and must exactly match the SHA-256 of the emitted .wasm file. expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() self.assertEqual(embedded_hash, expected_hash, - 'embedded WASM_SHA256 does not match actual .wasm SHA-256') + 'embedded wasm hash does not match actual .wasm SHA-256') def test_cross_origin_storage_disabled_by_default(self): """COS code must NOT appear when the flag is omitted (default off).""" @@ -15644,7 +15644,7 @@ def test_cross_origin_storage_not_emitted_for_node_target(self): emitted since the flag does nothing in this configuration. """ proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=node', '-o', 'hello.js'], stderr=PIPE) @@ -15655,7 +15655,7 @@ def test_cross_origin_storage_not_emitted_for_node_target(self): def test_cross_origin_storage_error_with_single_file(self): """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sSINGLE_FILE', '-o', 'hello.js'], @@ -15664,7 +15664,7 @@ def test_cross_origin_storage_error_with_single_file(self): def test_cross_origin_storage_error_without_async_compilation(self): """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sWASM_ASYNC_COMPILATION=0', '-o', 'hello.js'], @@ -15673,7 +15673,7 @@ def test_cross_origin_storage_error_without_async_compilation(self): def test_cross_origin_storage_warning_with_split_module(self): """CROSS_ORIGIN_STORAGE + SPLIT_MODULE must warn that secondary files are not covered.""" proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sSPLIT_MODULE', '-o', 'hello.js'], @@ -15684,7 +15684,7 @@ def test_cross_origin_storage_warning_with_split_module(self): def test_cross_origin_storage_warning_with_main_module(self): """CROSS_ORIGIN_STORAGE + MAIN_MODULE must warn that side modules are not covered.""" proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sMAIN_MODULE', '-o', 'hello.js'], @@ -15695,7 +15695,7 @@ def test_cross_origin_storage_warning_with_main_module(self): def test_cross_origin_storage_warning_with_side_module(self): """CROSS_ORIGIN_STORAGE + SIDE_MODULE must warn: no JS glue is emitted.""" proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sSIDE_MODULE', '-o', 'hello.wasm'], stderr=PIPE) @@ -15705,14 +15705,14 @@ def test_cross_origin_storage_warning_with_side_module(self): def test_cross_origin_storage_hash_changes_with_content(self): """Two different programs must produce different embedded hashes.""" self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) js_a = read_file('hello.js') hash_a = re.search(r"value:\s*'([0-9a-f]{64})'", js_a).group(1) self.run_process([EMCC, test_file('hello_world_small.c'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'small.js']) js_b = read_file('small.js') @@ -15731,7 +15731,7 @@ def test_cross_origin_storage_origins_default_is_global(self): Globally available; the user only needs -sCROSS_ORIGIN_STORAGE=1. """ self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) self.assertContained("origins: '*'", read_file('hello.js')) @@ -15742,7 +15742,7 @@ def test_cross_origin_storage_origins_explicit_wildcard(self): This matches the implicit default. """ self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', "-sCROSS_ORIGIN_STORAGE_ORIGINS=['*']", '-o', 'hello.js']) @@ -15751,7 +15751,7 @@ def test_cross_origin_storage_origins_explicit_wildcard(self): def test_cross_origin_storage_origins_explicit_list(self): """An explicit origins list must be emitted as a JS array.""" self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com', '-o', 'hello.js']) @@ -15763,7 +15763,7 @@ def test_cross_origin_storage_origins_explicit_list(self): def test_cross_origin_storage_origins_same_site(self): """Empty origins list must omit the origins key entirely (same-site only).""" self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=[]', '-o', 'hello.js']) @@ -15776,7 +15776,7 @@ def test_cross_origin_storage_origins_error_mixed_wildcard(self): """Mixing '*' with explicit origins must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]', '-o', 'hello.js'], @@ -15786,7 +15786,7 @@ def test_cross_origin_storage_origins_error_invalid_origin(self): """A non-HTTPS or malformed origin must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]', '-o', 'hello.js'], @@ -15796,40 +15796,45 @@ def test_cross_origin_storage_origins_error_origin_with_path(self): """An origin with a path component must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]', '-o', 'hello.js'], 'is not a valid HTTPS origin') - def test_cross_origin_storage_wasm_sha256_module_property(self): - """Module['wasmSHA256'] must be set in the JS output and match the .wasm hash. + def test_cross_origin_storage_wasm_hash_module_property(self): + """Module['wasmHash'] must be set in the JS output and match the .wasm hash. Custom Module['instantiateWasm'] implementations bypass instantiateAsync() - and therefore cannot reach the COS fetch logic that lives there. They can - read Module['wasmSHA256'] instead to get the build-time hash without parsing - the JS source. + and can read Module['wasmHash'] to get the build-time hash object. """ self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE=1', + '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') # The property must be present with a 64-char hex value. - m = re.search(r"Module\['wasmSHA256'\]\s*=\s*'([0-9a-f]{64})'", js) - self.assertTrue(m, "Module['wasmSHA256'] not found in JS output") + m = re.search(r"Module\['wasmHash'\]\s*=\s*\{[^}]*value:\s*'([0-9a-f]{64})'", js) + self.assertTrue(m, "Module['wasmHash'] not found in JS output") embedded_hash = m.group(1) # It must equal the SHA-256 of the actual .wasm file. expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() self.assertEqual(embedded_hash, expected_hash, - "Module['wasmSHA256'] does not match the actual .wasm SHA-256") + "Module['wasmHash'] does not match the actual .wasm SHA-256") - def test_cross_origin_storage_wasm_sha256_absent_without_flag(self): - """Module['wasmSHA256'] must NOT appear when CROSS_ORIGIN_STORAGE is off.""" + def test_cross_origin_storage_wasm_hash_absent_without_flag(self): + """Module['wasmHash'] must NOT appear when CROSS_ORIGIN_STORAGE is off.""" self.run_process([EMCC, test_file('hello_world.cpp'), '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') - self.assertNotContained("Module['wasmSHA256']", js) + self.assertNotContained("Module['wasmHash']", js) + + def test_deprecated_settings(self): + err = self.run_process([EMCC, '-sMEMORY64', test_file('hello_world.c')], stderr=PIPE).stderr + self.assertContained('emcc: warning: MEMORY64 is deprecated (prefer the standard -m64 or --target=wasm64 flags). Please open a bug if you have a continuing need for this setting [-Wdeprecated]', err) + + err = self.run_process([EMCC, '-sUSE_PTHREADS', test_file('hello_world.c')], stderr=PIPE).stderr + self.assertContained('emcc: warning: USE_PTHREADS is deprecated (prefer the standard -pthread flag). Please open a bug if you have a continuing need for this setting [-Wdeprecated]', err) diff --git a/tools/link.py b/tools/link.py index 31e5a54f7ad30..eb2138d7246be 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1210,34 +1210,21 @@ def limit_incoming_module_api(): exit_with_error('cannot have both WASM=2 and SINGLE_FILE enabled at the same time') if settings.CROSS_ORIGIN_STORAGE: - if settings.SINGLE_FILE: - exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE (the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on)') if not settings.ENVIRONMENT_MAY_BE_WEB: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web (navigator.crossOriginStorage is not available outside the browser)') - if not settings.WASM_ASYNC_COMPILATION: - exit_with_error('CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0 (synchronous instantiation does not use the COS fetch path)') if settings.SPLIT_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') if settings.MAIN_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') if settings.SIDE_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') - # Resolve and validate CROSS_ORIGIN_STORAGE_ORIGINS. - # The default in settings.js is [] (empty sentinel). When the user has - # not explicitly passed -sCROSS_ORIGIN_STORAGE_ORIGINS we default to ['*'] - # (globally available), which is the appropriate mode for widely-shared - # public Wasm binaries. An explicit =[] means same-site only. - if 'CROSS_ORIGIN_STORAGE_ORIGINS' not in user_settings: - settings.CROSS_ORIGIN_STORAGE_ORIGINS = ['*'] origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS - if not isinstance(origins, list): - exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS must be a list, e.g. https://example.com or https://a.com,https://b.com") if '*' in origins and len(origins) > 1: exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") for o in origins: if o == '*': continue - # Each explicit origin must be a valid serialised HTTPS origin: + # Each explicit origin must be a valid serialized HTTPS origin: # scheme "https://", host, optional ":port", no path/query/fragment. if not re.fullmatch(r'https://[^/]+(:\d+)?', o): exit_with_error(f"CROSS_ORIGIN_STORAGE_ORIGINS: {o!r} is not a valid HTTPS origin (expected 'https://host' or 'https://host:port')") @@ -1922,19 +1909,6 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat bindgen_jslib = building.run_wasm_bindgen(in_wasm) settings.JS_LIBRARIES.append(bindgen_jslib) - # Compute the SHA-256 of the wasm binary before phase_emscript so the hash - # is available to the JS-glue template as {{{ WASM_SHA256 }}}. We read - # in_wasm here (the pre-binaryen binary); after phase_binaryen we recompute - # the hash and patch the JS if binaryen changed the binary. - if settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB: - if os.path.exists(in_wasm): - with open(in_wasm, 'rb') as f: - wasm_bytes_pre = f.read() - settings.WASM_SHA256 = hashlib.sha256(wasm_bytes_pre).hexdigest() - logger.debug(f'CROSS_ORIGIN_STORAGE: pre-binaryen wasm SHA-256 = {settings.WASM_SHA256}') - else: - logger.warning('CROSS_ORIGIN_STORAGE: wasm file not found for hashing; WASM_SHA256 will be empty') - settings.WASM_SHA256 = '' metadata = phase_emscript(in_wasm, wasm_target, js_syms, base_metadata) @@ -1949,22 +1923,19 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat phase_binaryen(target, options, wasm_target) - # After binaryen, recompute the hash from the final wasm and patch the JS - # if binaryen changed the binary (so the embedded hash stays accurate). - if final_js and settings.CROSS_ORIGIN_STORAGE and settings.ENVIRONMENT_MAY_BE_WEB and settings.WASM_SHA256: + # Compute the hash of the final wasm (after binaryen) and substitute the + # <<< WASM_HASH_ALGORITHM >>> / <<< WASM_HASH_VALUE >>> placeholders that + # preamble.js left in the generated JS. + if final_js and settings.CROSS_ORIGIN_STORAGE: if os.path.exists(wasm_target): - with open(wasm_target, 'rb') as f: - wasm_bytes_post = f.read() - post_hash = hashlib.sha256(wasm_bytes_post).hexdigest() - if post_hash != settings.WASM_SHA256: - logger.debug(f'CROSS_ORIGIN_STORAGE: binaryen changed wasm; updating SHA-256 to {post_hash}') - js_content = read_file(final_js) - js_content = js_content.replace(f"value: '{settings.WASM_SHA256}'", - f"value: '{post_hash}'") - js_content = js_content.replace(f"Module['wasmSHA256'] = '{settings.WASM_SHA256}'", - f"Module['wasmSHA256'] = '{post_hash}'") - write_file(final_js, js_content) - settings.WASM_SHA256 = post_hash + wasm_bytes = utils.read_binary(wasm_target) + wasm_hash_algorithm = 'SHA-256' + wasm_hash_value = hashlib.sha256(wasm_bytes).hexdigest() + logger.debug(f'CROSS_ORIGIN_STORAGE: wasm {wasm_hash_algorithm} = {wasm_hash_value}') + js_content = read_file(final_js) + js_content = do_replace(js_content, '<<< WASM_HASH_ALGORITHM >>>', wasm_hash_algorithm) + js_content = do_replace(js_content, '<<< WASM_HASH_VALUE >>>', wasm_hash_value) + write_file(final_js, js_content) # If we are not emitting any JS then we are all done now if options.oformat != OFormat.WASM: diff --git a/tools/settings.py b/tools/settings.py index 4394c472c623c..ba4200c1ab17f 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -150,6 +150,8 @@ ('LEGACY_VM_SUPPORT', 'MEMORY64', None), ('CROSS_ORIGIN', 'NO_DYNAMIC_EXECUTION', None), ('CROSS_ORIGIN', 'NO_PTHREADS', None), + ('CROSS_ORIGIN_STORAGE', 'SINGLE_FILE', 'the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on'), + ('CROSS_ORIGIN_STORAGE', 'NO_WASM_ASYNC_COMPILATION', 'synchronous instantiation does not use the COS fetch path'), ] EXPERIMENTAL_SETTINGS = { From 9285bb4cb8e899c13beb068c5c3aa0633604aed9 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 13:08:25 +0200 Subject: [PATCH 42/87] refactor: make COS instrumentation callbacks opt-in via INCOMING_MODULE_JS_API Add onCOSCacheHit, onCOSCacheMiss, onCOSStore to EXTRA_INCOMING_JS_API in cmdline.py so they are valid but not included by default. Wrap each call in preamble.js with expectToReceiveOnModule() guards so the callback code is only emitted when the user lists them in INCOMING_MODULE_JS_API. Update tests and docs accordingly. --- .../docs/compiling/CrossOriginStorage.rst | 9 +++++++-- src/preamble.js | 6 ++++++ test/test_other.py | 20 +++++++++++++++---- tools/cmdline.py | 3 +++ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 28cf1d1018baf..e08f8cc4f9242 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -212,7 +212,12 @@ When the page loads, the generated JavaScript follows this logic: Instrumentation callbacks ------------------------- -Three optional ``Module`` properties let you observe COS events at runtime: +Three optional ``Module`` properties let you observe COS events at runtime. +They are **opt-in**: to include the callback code in the output, list them in +``INCOMING_MODULE_JS_API`` at link time:: + + emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE \ + -sINCOMING_MODULE_JS_API=onCOSCacheHit,onCOSCacheMiss,onCOSStore .. code-block:: javascript @@ -223,7 +228,7 @@ Three optional ``Module`` properties let you observe COS events at runtime: }, // Called when the Wasm binary was not in COS and was fetched over the - // network. |hash| is the SHA-256 that missed; |url| is the fallback URL. + // network. |hash| is the hash that missed; |url| is the fallback URL. onCOSCacheMiss: (hash, url) => { console.log('Cache miss, SHA-256:', hash, 'fetched from:', url); }, diff --git a/src/preamble.js b/src/preamble.js index 92c303b862dac..0a86cd4f0034e 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -639,7 +639,9 @@ async function instantiateAsync(binary, binaryFile, imports) { // Cache hit — read the Blob and instantiate from its ArrayBuffer. var cosFile = await cosHandles[0].getFile(); var cosBytes = await cosFile.arrayBuffer(); +#if expectToReceiveOnModule('onCOSCacheHit') Module['onCOSCacheHit']?.(cosHash.value); +#endif return WebAssembly.instantiate(cosBytes, imports); } catch (cosErr) { if (cosErr.name === 'NotFoundError') { @@ -647,7 +649,9 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); var wasmBytes = await networkResponse.arrayBuffer(); +#if expectToReceiveOnModule('onCOSCacheMiss') Module['onCOSCacheMiss']?.(cosHash.value, binaryFile); +#endif // Fire-and-forget store; never block instantiation on the write. (async () => { try { @@ -664,7 +668,9 @@ async function instantiateAsync(binary, binaryFile, imports) { var writable = await writeHandles[0].createWritable(); await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); await writable.close(); +#if expectToReceiveOnModule('onCOSStore') Module['onCOSStore']?.(cosHash.value); +#endif } catch (storeErr) { err(`COS store failed: ${storeErr}`); } diff --git a/test/test_other.py b/test/test_other.py index 5b86b72311571..54bf3bab97397 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15614,10 +15614,10 @@ def test_cross_origin_storage_js_output(self): self.assertContained("'NotFoundError'", js) self.assertContained("'NotAllowedError'", js) - # Instrumentation callbacks invoked at each COS event. - self.assertContained("Module['onCOSCacheHit']", js) - self.assertContained("Module['onCOSCacheMiss']", js) - self.assertContained("Module['onCOSStore']", js) + # Callbacks are opt-in; must be absent from a default build. + self.assertNotContained("Module['onCOSCacheHit']", js) + self.assertNotContained("Module['onCOSCacheMiss']", js) + self.assertNotContained("Module['onCOSStore']", js) # The hash embedded in the JS must be a 64-char lowercase hex string … m = re.search(r"value:\s*'([0-9a-f]{64})'", js) @@ -15629,6 +15629,18 @@ def test_cross_origin_storage_js_output(self): self.assertEqual(embedded_hash, expected_hash, 'embedded wasm hash does not match actual .wasm SHA-256') + def test_cross_origin_storage_callbacks_opt_in(self): + """COS instrumentation callbacks are emitted only when opted in via INCOMING_MODULE_JS_API.""" + self.run_process([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE', + '-sENVIRONMENT=web', + '-sINCOMING_MODULE_JS_API=onCOSCacheHit,onCOSCacheMiss,onCOSStore', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertContained("Module['onCOSCacheHit']", js) + self.assertContained("Module['onCOSCacheMiss']", js) + self.assertContained("Module['onCOSStore']", js) + def test_cross_origin_storage_disabled_by_default(self): """COS code must NOT appear when the flag is omitted (default off).""" self.run_process([EMCC, test_file('hello_world.cpp'), diff --git a/tools/cmdline.py b/tools/cmdline.py index b66249da8f46a..4b38e18ea0081 100644 --- a/tools/cmdline.py +++ b/tools/cmdline.py @@ -47,6 +47,9 @@ 'onRealloc', 'onFree', 'onSbrkGrow', + 'onCOSCacheHit', + 'onCOSCacheMiss', + 'onCOSStore', ] logger = logging.getLogger('args') From c442eecb84073df647ca1a85dd077ad05d237ea4 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 16:10:22 +0200 Subject: [PATCH 43/87] Add browser tests --- .../docs/compiling/CrossOriginStorage.rst | 26 + test/browser_common.py | 10 + test/cross_origin_storage/Makefile | 2 +- test/cross_origin_storage/README.md | 2 +- test/cross_origin_storage/index.html | 2 +- test/cross_origin_storage/index.js | 1759 +++++++++++++++++ test/cross_origin_storage/index.wasm | Bin 0 -> 994 bytes test/cross_origin_storage/main.cpp | 2 +- test/setup_cos_extension.py | 172 ++ test/test_browser.py | 63 + test/test_other.py | 4 +- tools/settings.py | 2 + 12 files changed, 2038 insertions(+), 6 deletions(-) create mode 100644 test/cross_origin_storage/index.js create mode 100755 test/cross_origin_storage/index.wasm create mode 100755 test/setup_cos_extension.py diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index e08f8cc4f9242..9a6dc4dfc7881 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -247,6 +247,9 @@ the `Cross-Origin Storage extension `_, which injects a ``navigator.crossOriginStorage`` polyfill on every page. +Manual testing +-------------- + 1. Install the extension in Chrome. 2. Build your project with ``-sCROSS_ORIGIN_STORAGE -sENVIRONMENT=web``. 3. Serve the output over HTTP (e.g. with ``emrun`` or ``python3 -m http.server``). @@ -254,6 +257,29 @@ which injects a ``navigator.crossOriginStorage`` polyfill on every page. COS. Open the same page in a second tab or from a different origin: the module is loaded from COS without a network request. +Automated browser testing +-------------------------- + +The Emscripten browser test suite includes COS tests that run against the +polyfill extension. The extension must be available as an **unpacked** +directory (containing ``manifest.json``). A helper script downloads and +unpacks it automatically:: + + python3 test/setup_cos_extension.py + +Then run the tests, passing the printed path as ``EMTEST_COS_EXTENSION_PATH``:: + + EMTEST_COS_EXTENSION_PATH=$(python3 test/setup_cos_extension.py --quiet) \ + python3 test/runner.py \ + browser.test_cross_origin_storage_fallback \ + browser.test_cross_origin_storage_miss_then_hit + +``test_cross_origin_storage_fallback`` does not require the extension and +verifies that a ``-sCROSS_ORIGIN_STORAGE`` build loads correctly on browsers +where the COS API is absent. ``test_cross_origin_storage_miss_then_hit`` +requires the extension and exercises both the cache-miss store and cache-hit +paths in sequence. + Verifying the embedded hash ============================ diff --git a/test/browser_common.py b/test/browser_common.py index 97d0a55d5f2e1..522bc3cf42a23 100644 --- a/test/browser_common.py +++ b/test/browser_common.py @@ -66,6 +66,14 @@ EMTEST_HEADLESS = None EMTEST_CAPTURE_STDIO = int(os.getenv('EMTEST_CAPTURE_STDIO', '0')) +# Path to an unpacked Chrome extension implementing the Cross-Origin Storage +# polyfill. When set, the extension is loaded via --load-extension when +# launching a Chromium-based browser, enabling the COS browser test paths. +# Point this at a local clone of: +# https://github.com/web-ai-community/cross-origin-storage-extension +# (the directory that contains manifest.json). +EMTEST_COS_EXTENSION_PATH = os.getenv('EMTEST_COS_EXTENSION_PATH', '') + # Triggers the browser to restart after every given number of tests. # 0: Disabled (reuse the browser instance to run all tests. Default) # 1: Restart a fresh browser instance for every browser test. @@ -365,6 +373,8 @@ def configure_test_browser(): EMTEST_BROWSER += ' ' + ' '.join(config.default_flags) if EMTEST_HEADLESS == 1: EMTEST_BROWSER += f" {config.headless_flags}" + if EMTEST_COS_EXTENSION_PATH and is_chrome(): + EMTEST_BROWSER += f' --load-extension="{EMTEST_COS_EXTENSION_PATH}"' # Create a server and a web page. When a test runs, we tell the server about it, diff --git a/test/cross_origin_storage/Makefile b/test/cross_origin_storage/Makefile index 0c5e4216bb8bf..00d75aaee3a15 100644 --- a/test/cross_origin_storage/Makefile +++ b/test/cross_origin_storage/Makefile @@ -11,7 +11,7 @@ all: index.js index.js: main.cpp $(EMCC) main.cpp -o index.js \ - -sCROSS_ORIGIN_STORAGE=1 \ + -sCROSS_ORIGIN_STORAGE \ -sENVIRONMENT=web \ -sEXPORTED_RUNTIME_METHODS=ccall \ -sEXPORTED_FUNCTIONS=_greet \ diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md index c28f7f91c24af..5e8ecf1a24135 100644 --- a/test/cross_origin_storage/README.md +++ b/test/cross_origin_storage/README.md @@ -26,7 +26,7 @@ of the Wasm resource and the URL it was fetched from. ```bash emcc main.cpp -o index.js \ - -sCROSS_ORIGIN_STORAGE=1 \ + -sCROSS_ORIGIN_STORAGE \ -sENVIRONMENT=web \ -sEXPORTED_RUNTIME_METHODS=ccall \ -sEXPORTED_FUNCTIONS=_greet \ diff --git a/test/cross_origin_storage/index.html b/test/cross_origin_storage/index.html index ca7f592187b8f..0ca1b7989453e 100644 --- a/test/cross_origin_storage/index.html +++ b/test/cross_origin_storage/index.html @@ -81,7 +81,7 @@

Emscripten — Cross-Origin Storage example

diff --git a/test/cross_origin_storage/index.js b/test/cross_origin_storage/index.js new file mode 100644 index 0000000000000..890b272da8989 --- /dev/null +++ b/test/cross_origin_storage/index.js @@ -0,0 +1,1759 @@ +// include: shell.js +// include: minimum_runtime_check.js +(function() { + // "30.0.0" -> 300000 + function humanReadableVersionToPacked(str) { + str = str.split('-')[0]; // Remove any trailing part from e.g. "12.53.3-alpha" + var vers = str.split('.').slice(0, 3); + while(vers.length < 3) vers.push('00'); + vers = vers.map((n, i, arr) => n.padStart(2, '0')); + return vers.join(''); + } + // 300000 -> "30.0.0" + var packedVersionToHumanReadable = n => [n / 10000 | 0, (n / 100 | 0) % 100, n % 100].join('.'); + + var TARGET_NOT_SUPPORTED = 2147483647; + + // Note: We use a typeof check here instead of optional chaining using + // globalThis because older browsers might not have globalThis defined. + var currentNodeVersion = typeof process !== 'undefined' && process.versions?.node ? humanReadableVersionToPacked(process.versions.node) : TARGET_NOT_SUPPORTED; + if (currentNodeVersion < TARGET_NOT_SUPPORTED) { + throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); + } + if (currentNodeVersion < 2147483647) { + throw new Error(`This emscripten-generated code requires node v${ packedVersionToHumanReadable(2147483647) } (detected v${packedVersionToHumanReadable(currentNodeVersion)})`); + } + + var userAgent = typeof navigator !== 'undefined' && navigator.userAgent; + if (!userAgent) { + return; + } + + var currentSafariVersion = userAgent.includes("Safari/") && !userAgent.includes("Chrome/") && userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/) ? humanReadableVersionToPacked(userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentSafariVersion < 150000) { + throw new Error(`This emscripten-generated code requires Safari v${ packedVersionToHumanReadable(150000) } (detected v${currentSafariVersion})`); + } + + var currentFirefoxVersion = userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentFirefoxVersion < 79) { + throw new Error(`This emscripten-generated code requires Firefox v79 (detected v${currentFirefoxVersion})`); + } + + var currentChromeVersion = userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; + if (currentChromeVersion < 85) { + throw new Error(`This emscripten-generated code requires Chrome v85 (detected v${currentChromeVersion})`); + } +})(); + +// end include: minimum_runtime_check.js +// The Module object: Our interface to the outside world. We import +// and export values on it. There are various ways Module can be used: +// 1. Not defined. We create it here +// 2. A function parameter, function(moduleArg) => Promise +// 3. pre-run appended it, var Module = {}; ..generated code.. +// 4. External script tag defines var Module. +// We need to check if Module already exists (e.g. case 3 above). +// Substitution will be replaced with actual code on later stage of the build, +// this way Closure Compiler will not mangle it (e.g. case 4. above). +// Note that if you want to run closure, and also to use Module +// after the generated code, you will need to define var Module = {}; +// before the code. Then that object will be used in the code, and you +// can continue to use Module afterwards as well. +var Module = typeof Module != 'undefined' ? Module : {}; + +// Determine the runtime environment we are in. You can customize this by +// setting the ENVIRONMENT setting at compile time (see settings.js). + +// Attempt to auto-detect the environment +var ENVIRONMENT_IS_WEB = !!globalThis.window; +var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; +// N.b. Electron.js environment is simultaneously a NODE-environment, but +// also a web environment. +var ENVIRONMENT_IS_NODE = globalThis.process?.versions?.node && globalThis.process?.type != 'renderer'; +var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; + +// --pre-jses are emitted after the Module integration code, so that they can +// refer to Module (if they choose; they can also define Module) + + +var programArgs = []; +var thisProgram = './this.program'; +var quit_ = (status, toThrow) => { + throw toThrow; +}; + +// In MODULARIZE mode _scriptName needs to be captured already at the very top of the page immediately when the page is parsed, so it is generated there +// before the page load. In non-MODULARIZE modes generate it here. +var _scriptName = globalThis.document?.currentScript?.src; + +// `/` should be present at the end if `scriptDirectory` is not empty +var scriptDirectory = ''; +function locateFile(path) { + if (Module['locateFile']) { + return Module['locateFile'](path, scriptDirectory); + } + return scriptDirectory + path; +} + +// Hooks that are implemented differently in different runtime environments. +var readAsync, readBinary; + +if (ENVIRONMENT_IS_SHELL) { + +} else + +// Note that this includes Node.js workers when relevant (pthreads is enabled). +// Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and +// ENVIRONMENT_IS_NODE. +if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { + try { + scriptDirectory = new URL('.', _scriptName).href; // includes trailing slash + } catch { + // Must be a `blob:` or `data:` URL (e.g. `blob:http://site.com/etc/etc`), we cannot + // infer anything from them. + } + + if (!(globalThis.window || globalThis.WorkerGlobalScope)) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); + + { +// include: web_or_worker_shell_read.js +readAsync = async (url) => { + assert(!isFileURI(url), "readAsync does not work with file:// URLs"); + var response = await fetch(url, { credentials: 'same-origin' }); + if (response.ok) { + return response.arrayBuffer(); + } + throw new Error(response.status + ' : ' + response.url); + }; +// end include: web_or_worker_shell_read.js + } +} else +{ + throw new Error('environment detection error'); +} + +var out = console.log.bind(console); +var err = console.error.bind(console); + +var IDBFS = 'IDBFS is no longer included by default; build with -lidbfs.js'; +var PROXYFS = 'PROXYFS is no longer included by default; build with -lproxyfs.js'; +var WORKERFS = 'WORKERFS is no longer included by default; build with -lworkerfs.js'; +var FETCHFS = 'FETCHFS is no longer included by default; build with -lfetchfs.js'; +var ICASEFS = 'ICASEFS is no longer included by default; build with -licasefs.js'; +var JSFILEFS = 'JSFILEFS is no longer included by default; build with -ljsfilefs.js'; +var OPFS = 'OPFS is no longer included by default; build with -lopfs.js'; + +var NODEFS = 'NODEFS is no longer included by default; build with -lnodefs.js'; + +// perform assertions in shell.js after we set up out() and err(), as otherwise +// if an assertion fails it cannot print the message + +assert(!ENVIRONMENT_IS_WORKER, 'worker environment detected but not enabled at build time (add `worker` to `-sENVIRONMENT` to enable)'); + +assert(!ENVIRONMENT_IS_NODE, 'node environment detected but not enabled at build time (add `node` to `-sENVIRONMENT` to enable)'); + +assert(!ENVIRONMENT_IS_SHELL, 'shell environment detected but not enabled at build time (add `shell` to `-sENVIRONMENT` to enable)'); + +// end include: shell.js + +// include: preamble.js +// === Preamble library stuff === + +// Documentation for the public APIs defined in this file must be updated in: +// site/source/docs/api_reference/preamble.js.rst +// A prebuilt local version of the documentation is available at: +// site/build/text/docs/api_reference/preamble.js.txt +// You can also build docs locally as HTML or other formats in site/ +// An online HTML version (which may be of a different version of Emscripten) +// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html + +var wasmBinary; + +if (!globalThis.WebAssembly) { + err('no native wasm support detected'); +} + +// Wasm globals + +//======================================== +// Runtime essentials +//======================================== + +// whether we are quitting the application. no code should run after this. +// set in exit() and abort() +var ABORT = false; + +// set by exit() and abort(). Passed to 'onExit' handler. +// NOTE: This is also used as the process return code in shell environments +// but only when noExitRuntime is false. +var EXITSTATUS; + +// In STRICT mode, we only define assert() when ASSERTIONS is set. i.e. we +// don't define it at all in release modes. This matches the behaviour of +// MINIMAL_RUNTIME. +// TODO(sbc): Make this the default even without STRICT enabled. +/** @type {function(*, string=)} */ +function assert(condition, text) { + if (!condition) { + abort('Assertion failed' + (text ? ': ' + text : '')); + } +} + +// We used to include malloc/free by default in the past. Show a helpful error in +// builds with assertions. +function _malloc() { + abort('malloc() called but not included in the build - add `_malloc` to EXPORTED_FUNCTIONS'); +} +function _free() { + // Show a helpful error since we used to include free by default in the past. + abort('free() called but not included in the build - add `_free` to EXPORTED_FUNCTIONS'); +} + +/** + * Indicates whether filename is delivered via file protocol (as opposed to http/https) + * @noinline + */ +var isFileURI = (filename) => filename.startsWith('file://'); + +// include: runtime_common.js +// include: runtime_stack_check.js +// Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode. +function writeStackCookie() { + var max = _emscripten_stack_get_end(); + assert((max & 3) == 0); + // If the stack ends at address zero we write our cookies 4 bytes into the + // stack. This prevents interference with SAFE_HEAP and ASAN which also + // monitor writes to address zero. + if (max == 0) { + max += 4; + } + // The stack grow downwards towards _emscripten_stack_get_end. + // We write cookies to the final two words in the stack and detect if they are + // ever overwritten. + HEAPU32[((max)>>2)] = 0x02135467; + HEAPU32[(((max)+(4))>>2)] = 0x89BACDFE; + // Also test the global address 0 for integrity. + HEAPU32[((0)>>2)] = 1668509029; +} + +function checkStackCookie() { + if (ABORT) return; + var max = _emscripten_stack_get_end(); + // See writeStackCookie(). + if (max == 0) { + max += 4; + } + var cookie1 = HEAPU32[((max)>>2)]; + var cookie2 = HEAPU32[(((max)+(4))>>2)]; + if (cookie1 != 0x02135467 || cookie2 != 0x89BACDFE) { + abort(`Stack overflow! Stack cookie has been overwritten at ${ptrToString(max)}, expected hex dwords 0x89BACDFE and 0x2135467, but received ${ptrToString(cookie2)} ${ptrToString(cookie1)}`); + } + // Also test the global address 0 for integrity. + if (HEAPU32[((0)>>2)] != 0x63736d65 /* 'emsc' */) { + abort('Runtime error: The application has corrupted its heap memory area (address zero)!'); + } +} +// end include: runtime_stack_check.js +// include: runtime_exceptions.js +// Base Emscripten EH error class +class EmscriptenEH {} + +class EmscriptenSjLj extends EmscriptenEH {} + +// end include: runtime_exceptions.js +// include: runtime_debug.js +var runtimeDebug = true; // Switch to false at runtime to disable logging at the right times + +// Used by XXXXX_DEBUG settings to output debug messages. +function dbg(...args) { + if (!runtimeDebug && typeof runtimeDebug != 'undefined') return; + // TODO(sbc): Make this configurable somehow. Its not always convenient for + // logging to show up as warnings. + console.warn(...args); +} + +// Endianness check +(() => { + var h16 = new Int16Array(1); + var h8 = new Int8Array(h16.buffer); + h16[0] = 0x6373; + if (h8[0] !== 0x73 || h8[1] !== 0x63) abort('Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)'); +})(); + +function consumedModuleProp(prop) { + if (!Object.getOwnPropertyDescriptor(Module, prop)) { + Object.defineProperty(Module, prop, { + configurable: true, + set() { + abort(`Attempt to set \`Module.${prop}\` after it has already been processed. This can happen, for example, when code is injected via '--post-js' rather than '--pre-js'`); + + } + }); + } +} + +function makeInvalidEarlyAccess(name) { + return () => assert(false, `call to '${name}' via reference taken before Wasm module initialization`); + +} + +function ignoredModuleProp(prop) { + if (Object.getOwnPropertyDescriptor(Module, prop)) { + abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`); + } +} + +// forcing the filesystem exports a few things by default +function isExportedByForceFilesystem(name) { + return name === 'FS_createPath' || + name === 'FS_createDataFile' || + name === 'FS_createPreloadedFile' || + name === 'FS_preloadFile' || + name === 'FS_unlink' || + name === 'addRunDependency' || + // The old FS has some functionality that WasmFS lacks. + name === 'FS_createLazyFile' || + name === 'FS_createDevice' || + name === 'removeRunDependency'; +} + +/** + * Intercept access to a symbols in the global symbol. This enables us to give + * informative warnings/errors when folks attempt to use symbols they did not + * include in their build, or no symbols that no longer exist. + * + * We don't define this in MODULARIZE mode since in that mode emscripten symbols + * are never placed in the global scope. + */ +function hookGlobalSymbolAccess(sym, func) { + if (!Object.getOwnPropertyDescriptor(globalThis, sym)) { + Object.defineProperty(globalThis, sym, { + configurable: true, + get() { + func(); + return undefined; + } + }); + } +} + +function missingGlobal(sym, msg) { + hookGlobalSymbolAccess(sym, () => { + warnOnce(`\`${sym}\` is no longer defined by emscripten. ${msg}`); + }); +} + +missingGlobal('buffer', 'Please use HEAP8.buffer or wasmMemory.buffer'); +missingGlobal('asm', 'Please use wasmExports instead'); + +function missingLibrarySymbol(sym) { + hookGlobalSymbolAccess(sym, () => { + // Can't `abort()` here because it would break code that does runtime + // checks. e.g. `if (typeof SDL === 'undefined')`. + var msg = `\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`; + // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE requires the name as it appears in + // library.js, which means $name for a JS name with no prefix, or name + // for a JS name like _name. + var librarySymbol = sym; + if (!librarySymbol.startsWith('_')) { + librarySymbol = '$' + sym; + } + msg += ` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`; + if (isExportedByForceFilesystem(sym)) { + msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; + } + warnOnce(msg); + }); + + // Any symbol that is not included from the JS library is also (by definition) + // not exported on the Module object. + unexportedRuntimeSymbol(sym); +} + +function unexportedRuntimeSymbol(sym) { + if (!Object.getOwnPropertyDescriptor(Module, sym)) { + Object.defineProperty(Module, sym, { + configurable: true, + get() { + var msg = `'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`; + if (isExportedByForceFilesystem(sym)) { + msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; + } + abort(msg); + }, + }); + } +} + +// end include: runtime_debug.js +// Memory management + +var runtimeInitialized = false; + + + +function updateMemoryViews() { + var b = wasmMemory.buffer; + HEAP8 = new Int8Array(b); + HEAP16 = new Int16Array(b); + HEAPU8 = new Uint8Array(b); + HEAPU16 = new Uint16Array(b); + HEAP32 = new Int32Array(b); + HEAPU32 = new Uint32Array(b); + HEAPF32 = new Float32Array(b); + HEAPF64 = new Float64Array(b); + HEAP64 = new BigInt64Array(b); + HEAPU64 = new BigUint64Array(b); +} + +// include: memoryprofiler.js +// end include: memoryprofiler.js +// end include: runtime_common.js +assert(globalThis.Int32Array && globalThis.Float64Array && Int32Array.prototype.subarray && Int32Array.prototype.set, + 'JS engine does not provide full typed array support'); + +function preRun() { + if (Module['preRun']) { + if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; + while (Module['preRun'].length) { + addOnPreRun(Module['preRun'].shift()); + } + } + consumedModuleProp('preRun'); + // Begin ATPRERUNS hooks + callRuntimeCallbacks(onPreRuns); + // End ATPRERUNS hooks +} + +function initRuntime() { + assert(!runtimeInitialized); + runtimeInitialized = true; + + checkStackCookie(); + + // No ATINITS hooks + + wasmExports['__wasm_call_ctors'](); + + // No ATPOSTCTORS hooks +} + +function postRun() { + checkStackCookie(); + // PThreads reuse the runtime from the main thread. + + if (Module['postRun']) { + if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; + while (Module['postRun'].length) { + addOnPostRun(Module['postRun'].shift()); + } + } + consumedModuleProp('postRun'); + + // Begin ATPOSTRUNS hooks + callRuntimeCallbacks(onPostRuns); + // End ATPOSTRUNS hooks +} + +/** + * @param {string|number=} what + */ +function abort(what) { + Module['onAbort']?.(what); + + what = `Aborted(${what})`; + // TODO(sbc): Should we remove printing and leave it up to whoever + // catches the exception? + err(what); + + ABORT = true; + + // Use a wasm runtime error, because a JS error might be seen as a foreign + // exception, which means we'd run destructors on it. We need the error to + // simply make the program stop. + // FIXME This approach does not work in Wasm EH because it currently does not assume + // all RuntimeErrors are from traps; it decides whether a RuntimeError is from + // a trap or not based on a hidden field within the object. So at the moment + // we don't have a way of throwing a wasm trap from JS. TODO Make a JS API that + // allows this in the wasm spec. + + // Suppress closure compiler warning here. Closure compiler's builtin extern + // definition for WebAssembly.RuntimeError claims it takes no arguments even + // though it can. + // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure gets fixed. + /** @suppress {checkTypes} */ + var e = new WebAssembly.RuntimeError(what); + + // Throw the error whether or not MODULARIZE is set because abort is used + // in code paths apart from instantiation where an exception is expected + // to be thrown when abort is called. + throw e; +} + +// show errors on likely calls to FS when it was not included +function fsMissing() { + abort('Filesystem support (FS) was not included. The problem is that you are using files from JS, but files were not used from C/C++, so filesystem support was not auto-included. You can force-include filesystem support with -sFORCE_FILESYSTEM'); +} +var FS = { + init: fsMissing, + createDataFile: fsMissing, + createPreloadedFile: fsMissing, + createLazyFile: fsMissing, + open: fsMissing, + mkdev: fsMissing, + registerDevice: fsMissing, + analyzePath: fsMissing, + ErrnoError: fsMissing, +}; + + +function createExportWrapper(name, nargs) { + return (...args) => { + assert(runtimeInitialized, `native function \`${name}\` called before runtime initialization`); + var f = wasmExports[name]; + assert(f, `exported native function \`${name}\` not found`); + // Only assert for too many arguments. Too few can be valid since the missing arguments will be zero filled. + assert(args.length <= nargs, `native function \`${name}\` called with ${args.length} args but expects ${nargs}`); + return f(...args); + }; +} + +var wasmBinaryFile; + +function findWasmBinary() { + return locateFile('index.wasm'); +} + +function getBinarySync(file) { + if (file == wasmBinaryFile && wasmBinary) { + return new Uint8Array(wasmBinary); + } + if (readBinary) { + return readBinary(file); + } + // Throwing a plain string here, even though it not normally advisable since + // this gets turning into an `abort` in instantiateArrayBuffer. + throw 'both async and sync fetching of the wasm failed'; +} + +async function getWasmBinary(binaryFile) { + // If we don't have the binary yet, load it asynchronously using readAsync. + if (!wasmBinary) { + // Fetch the binary using readAsync + try { + var response = await readAsync(binaryFile); + return new Uint8Array(response); + } catch { + // Fall back to getBinarySync below; + } + } + + // Otherwise, getBinarySync should be able to get it synchronously + return getBinarySync(binaryFile); +} + +async function instantiateArrayBuffer(binaryFile, imports) { + try { + var binary = await getWasmBinary(binaryFile); + var instance = await WebAssembly.instantiate(binary, imports); + return instance; + } catch (reason) { + err(`failed to asynchronously prepare wasm: ${reason}`); + + // Warn on some common problems. + if (isFileURI(binaryFile)) { + err(`warning: Loading from a file URI (${binaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`); + } + abort(reason); + } +} + +async function instantiateAsync(binary, binaryFile, imports) { + // Cross-Origin Storage (COS) progressive enhancement. + // https://github.com/WICG/cross-origin-storage + // + // COS is only beneficial when this .wasm binary is byte-identical across + // many origins — i.e. a popular library loaded by many independent sites. + // Application-specific Wasm gains nothing from COS that the normal HTTP + // cache does not already provide. + // + // The SHA-256 hash of the final .wasm binary is computed at link time and + // embedded here as a build-time constant. At runtime we feature-detect the + // browser COS API via `'crossOriginStorage' in navigator`, then call + // navigator.crossOriginStorage.requestFileHandles() with the hash object + // required by the spec ({ algorithm: 'SHA-256', value: '' }). + // + // Cache-hit path: + // requestFileHandles() succeeds → getFile() → arrayBuffer() → instantiate + // + // Cache-miss path (NotFoundError): + // fetch() the wasm over the network → instantiate → store in COS. + // The origins field is controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS + // (default: '*', globally available). The store is fire-and-forget so + // it never delays startup. + // + // Visibility and security: the spec allows widening visibility (e.g. a + // restricted entry promoted to globally available) but never narrowing it. + // Because storing always requires writing the actual bytes, no third party + // can probe the cache to discover whether a restricted entry was previously + // stored by another origin — a cache hit is only possible after an explicit + // write that provided the content. + // + // Any other error (NotAllowedError, network failure, …) falls through to the + // standard Emscripten streaming path so the page always loads. + var cosHash = { algorithm: 'SHA-256', value: 'a617cb51176b7e0c729fb776ff688310ecbe035a0da79fcac66b1bf98028e793' }; + if (cosHash.value && 'crossOriginStorage' in navigator) { + try { + var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); + // Cache hit — read the Blob and instantiate from its ArrayBuffer. + var cosFile = await cosHandles[0].getFile(); + var cosBytes = await cosFile.arrayBuffer(); + // Optional instrumentation callback: Module['onCOSCacheHit'](hash) + // Called when the Wasm binary is served from the cross-origin cache. + if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](cosHash.value); + return WebAssembly.instantiate(cosBytes, imports); + } catch (cosErr) { + if (cosErr.name === 'NotFoundError') { + // Cache miss — fetch normally, then store in COS for future consumers. + try { + var networkResponse = await fetch(binaryFile, { credentials: 'same-origin' }); + var wasmBytes = await networkResponse.arrayBuffer(); + // Optional instrumentation callback: Module['onCOSCacheMiss'](hash, url) + // Called when the Wasm binary is not in COS and is fetched over the network. + if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](cosHash.value, binaryFile); + // Fire-and-forget store; never block instantiation on the write. + (async () => { + try { + var writeHandles = await navigator.crossOriginStorage.requestFileHandles( + [cosHash], + { create: true, origins: '*' }, + ); + var writable = await writeHandles[0].createWritable(); + await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); + await writable.close(); + // Optional instrumentation callback: Module['onCOSStore'](hash) + // Called after the Wasm binary has been successfully written to COS. + if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](cosHash.value); + } catch (storeErr) { + err(`COS store failed: ${storeErr}`); + } + })(); + return WebAssembly.instantiate(wasmBytes, imports); + } catch (fetchErr) { + // Network fetch failed; fall through to the standard path. + err(`COS fallback fetch failed: ${fetchErr}`); + } + } else if (cosErr.name === 'NotAllowedError') { + err(`COS: permission denied.`); + } else { + err(`Cross-Origin Storage lookup failed: ${cosErr}`); + } + } + } + if (!binary + ) { + try { + var response = fetch(binaryFile, { credentials: 'same-origin' }); + var instantiationResult = await WebAssembly.instantiateStreaming(response, imports); + return instantiationResult; + } catch (reason) { + // We expect the most common failure cause to be a bad MIME type for the binary, + // in which case falling back to ArrayBuffer instantiation should work. + err(`wasm streaming compile failed: ${reason}`); + err('falling back to ArrayBuffer instantiation'); + // fall back of instantiateArrayBuffer below + }; + } + return instantiateArrayBuffer(binaryFile, imports); +} + +function getWasmImports() { + // prepare imports + var imports = { + 'env': wasmImports, + 'wasi_snapshot_preview1': wasmImports, + }; + return imports; +} + +// Create the wasm instance. +// Receives the wasm imports, returns the exports. +async function createWasm() { + // Load the wasm module and create an instance of using native support in the JS engine. + // handle a generated wasm instance, receiving its exports and + // performing other necessary setup + /** @param {WebAssembly.Module=} module*/ + function receiveInstance(instance, module) { + wasmExports = instance.exports; + + assignWasmExports(wasmExports); + + updateMemoryViews(); + + removeRunDependency('wasm-instantiate'); + return wasmExports; + } + addRunDependency('wasm-instantiate'); + + // Prefer streaming instantiation if available. + // Async compilation can be confusing when an error on the page overwrites Module + // (for example, if the order of elements is wrong, and the one defining Module is + // later), so we save Module and check it later. + var trueModule = Module; + function receiveInstantiationResult(result) { + // 'result' is a ResultObject object which has both the module and instance. + // receiveInstance() will swap in the exports (to Module.asm) so they can be called + assert(Module === trueModule, 'the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?'); + trueModule = null; + // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. + // When the regression is fixed, can restore the above PTHREADS-enabled path. + return receiveInstance(result['instance']); + } + + var info = getWasmImports(); + + // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback + // to manually instantiate the Wasm module themselves. This allows pages to + // run the instantiation parallel to any other async startup actions they are + // performing. + // Also pthreads and wasm workers initialize the wasm instance through this + // path. + if (Module['instantiateWasm']) { + return new Promise((resolve, reject) => { + try { + Module['instantiateWasm'](info, (inst, mod) => { + resolve(receiveInstance(inst, mod)); + }); + } catch(e) { + err(`Module.instantiateWasm callback failed with error: ${e}`); + reject(e); + } + }); + } + + wasmBinaryFile ??= findWasmBinary(); + var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info); + var exports = receiveInstantiationResult(result); + return exports; +} + +// end include: preamble.js + +// Begin JS library code + + + class ExitStatus { + name = 'ExitStatus'; + constructor(status) { + this.message = `Program terminated with exit(${status})`; + this.status = status; + } + } + + /** @type {!Int16Array} */ + var HEAP16; + + /** @type {!Int32Array} */ + var HEAP32; + + /** not-@type {!BigInt64Array} */ + var HEAP64; + + /** @type {!Int8Array} */ + var HEAP8; + + /** @type {!Float32Array} */ + var HEAPF32; + + /** @type {!Float64Array} */ + var HEAPF64; + + /** @type {!Uint16Array} */ + var HEAPU16; + + /** @type {!Uint32Array} */ + var HEAPU32; + + /** not-@type {!BigUint64Array} */ + var HEAPU64; + + /** @type {!Uint8Array} */ + var HEAPU8; + + var callRuntimeCallbacks = (callbacks) => { + while (callbacks.length > 0) { + // Pass the module as the first argument. + callbacks.shift()(Module); + } + }; + var onPostRuns = []; + var addOnPostRun = (cb) => onPostRuns.push(cb); + + var onPreRuns = []; + var addOnPreRun = (cb) => onPreRuns.push(cb); + + var runDependencies = 0; + + + var dependenciesFulfilled = null; + + var runDependencyTracking = { + }; + + var runDependencyWatcher = null; + var removeRunDependency = (id) => { + runDependencies--; + + Module['monitorRunDependencies']?.(runDependencies); + + assert(id, 'removeRunDependency requires an ID'); + assert(runDependencyTracking[id]); + delete runDependencyTracking[id]; + if (runDependencies == 0) { + if (runDependencyWatcher !== null) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + } + if (dependenciesFulfilled) { + var callback = dependenciesFulfilled; + dependenciesFulfilled = null; + callback(); // can add another dependenciesFulfilled + } + } + }; + + + var addRunDependency = (id) => { + runDependencies++; + + Module['monitorRunDependencies']?.(runDependencies); + + assert(id, 'addRunDependency requires an ID') + assert(!runDependencyTracking[id]); + runDependencyTracking[id] = 1; + if (runDependencyWatcher === null && globalThis.setInterval) { + // Check for missing dependencies every few seconds + runDependencyWatcher = setInterval(() => { + if (ABORT) { + clearInterval(runDependencyWatcher); + runDependencyWatcher = null; + return; + } + var shown = false; + for (var dep in runDependencyTracking) { + if (!shown) { + shown = true; + err('still waiting on run dependencies:'); + } + err(`dependency: ${dep}`); + } + if (shown) { + err('(end of list)'); + } + }, 10000); + } + }; + + + + /** + * @param {number} ptr + * @param {string} type + */ + function getValue(ptr, type = 'i8') { + if (type.endsWith('*')) type = '*'; + switch (type) { + case 'i1': return HEAP8[ptr]; + case 'i8': return HEAP8[ptr]; + case 'i16': return HEAP16[((ptr)>>1)]; + case 'i32': return HEAP32[((ptr)>>2)]; + case 'i64': return HEAP64[((ptr)>>3)]; + case 'float': return HEAPF32[((ptr)>>2)]; + case 'double': return HEAPF64[((ptr)>>3)]; + case '*': return HEAPU32[((ptr)>>2)]; + default: abort(`invalid type for getValue: ${type}`); + } + } + + var noExitRuntime = true; + + function ptrToString(ptr) { + assert(typeof ptr === 'number', `ptrToString expects a number, got ${typeof ptr}`); + // Convert to 32-bit unsigned value + ptr >>>= 0; + return '0x' + ptr.toString(16).padStart(8, '0'); + } + + + + /** + * @param {number} ptr + * @param {number} value + * @param {string} type + */ + function setValue(ptr, value, type = 'i8') { + if (type.endsWith('*')) type = '*'; + switch (type) { + case 'i1': HEAP8[ptr] = value; break; + case 'i8': HEAP8[ptr] = value; break; + case 'i16': HEAP16[((ptr)>>1)] = value; break; + case 'i32': HEAP32[((ptr)>>2)] = value; break; + case 'i64': HEAP64[((ptr)>>3)] = BigInt(value); break; + case 'float': HEAPF32[((ptr)>>2)] = value; break; + case 'double': HEAPF64[((ptr)>>3)] = value; break; + case '*': HEAPU32[((ptr)>>2)] = value; break; + default: abort(`invalid type for setValue: ${type}`); + } + } + + var stackRestore = (val) => __emscripten_stack_restore(val); + + var stackSave = () => _emscripten_stack_get_current(); + + var warnOnce = (text) => { + warnOnce.shown ||= {}; + if (!warnOnce.shown[text]) { + warnOnce.shown[text] = 1; + err(text); + } + }; + + + + var getCFunc = (ident) => { + var func = Module['_' + ident]; // closure exported function + assert(func, `Cannot call unknown function ${ident}, make sure it is exported`); + return func; + }; + + var writeArrayToMemory = (array, buffer) => { + assert(array.length >= 0, 'writeArrayToMemory array must have a length (should be an array or typed array)') + HEAP8.set(array, buffer); + }; + + var lengthBytesUTF8 = (str) => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code + // unit, not a Unicode code point of the character! So decode + // UTF16->UTF32->UTF8. + // See http://unicode.org/faq/utf_bom.html#utf16-3 + var c = str.charCodeAt(i); // possibly a lead surrogate + if (c <= 0x7F) { + len++; + } else if (c <= 0x7FF) { + len += 2; + } else if (c >= 0xD800 && c <= 0xDFFF) { + len += 4; ++i; + } else { + len += 3; + } + } + return len; + }; + + var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { + assert(typeof str === 'string', `stringToUTF8Array expects a string (got ${typeof str})`); + // Parameter maxBytesToWrite is not optional. Negative values, 0, null, + // undefined and false each don't write out any bytes. + if (!(maxBytesToWrite > 0)) + return 0; + + var startIdx = outIdx; + var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. + for (var i = 0; i < str.length; ++i) { + // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description + // and https://www.ietf.org/rfc/rfc2279.txt + // and https://tools.ietf.org/html/rfc3629 + var u = str.codePointAt(i); + if (u <= 0x7F) { + if (outIdx >= endIdx) break; + heap[outIdx++] = u; + } else if (u <= 0x7FF) { + if (outIdx + 1 >= endIdx) break; + heap[outIdx++] = 0xC0 | (u >> 6); + heap[outIdx++] = 0x80 | (u & 63); + } else if (u <= 0xFFFF) { + if (outIdx + 2 >= endIdx) break; + heap[outIdx++] = 0xE0 | (u >> 12); + heap[outIdx++] = 0x80 | ((u >> 6) & 63); + heap[outIdx++] = 0x80 | (u & 63); + } else { + if (outIdx + 3 >= endIdx) break; + if (u > 0x10FFFF) warnOnce(`Invalid Unicode code point ${ptrToString(u)} encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).`); + heap[outIdx++] = 0xF0 | (u >> 18); + heap[outIdx++] = 0x80 | ((u >> 12) & 63); + heap[outIdx++] = 0x80 | ((u >> 6) & 63); + heap[outIdx++] = 0x80 | (u & 63); + // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. + // We need to manually skip over the second code unit for correct iteration. + i++; + } + } + // Null-terminate the pointer to the buffer. + heap[outIdx] = 0; + return outIdx - startIdx; + }; + var stringToUTF8 = (str, outPtr, maxBytesToWrite) => { + assert(typeof maxBytesToWrite == 'number', 'stringToUTF8 requires a third parameter that specifies the length of the output buffer'); + return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); + }; + + var stackAlloc = (sz) => __emscripten_stack_alloc(sz); + var stringToUTF8OnStack = (str) => { + var size = lengthBytesUTF8(str) + 1; + var ret = stackAlloc(size); + stringToUTF8(str, ret, size); + return ret; + }; + + + + + var UTF8Decoder = globalThis.TextDecoder && new TextDecoder(); + + var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { + var maxIdx = idx + maxBytesToRead; + if (ignoreNul) return maxIdx; + // TextDecoder needs to know the byte length in advance, it doesn't stop on + // null terminator by itself. + // As a tiny code save trick, compare idx against maxIdx using a negation, + // so that maxBytesToRead=undefined/NaN means Infinity. + while (heapOrArray[idx] && !(idx >= maxIdx)) ++idx; + return idx; + }; + + + /** + * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given + * array that contains uint8 values, returns a copy of that string as a + * Javascript String object. + * heapOrArray is either a regular array, or a JavaScript typed array view. + * @param {number=} idx + * @param {number=} maxBytesToRead + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ + var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead, ignoreNul) => { + + var endPtr = findStringEnd(heapOrArray, idx, maxBytesToRead, ignoreNul); + + // When using conditional TextDecoder, skip it for short strings as the overhead of the native call is not worth it. + if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) { + return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr)); + } + var str = ''; + while (idx < endPtr) { + // For UTF8 byte structure, see: + // http://en.wikipedia.org/wiki/UTF-8#Description + // https://www.ietf.org/rfc/rfc2279.txt + // https://tools.ietf.org/html/rfc3629 + var u0 = heapOrArray[idx++]; + if (!(u0 & 0x80)) { str += String.fromCharCode(u0); continue; } + var u1 = heapOrArray[idx++] & 63; + if ((u0 & 0xE0) == 0xC0) { str += String.fromCharCode(((u0 & 31) << 6) | u1); continue; } + var u2 = heapOrArray[idx++] & 63; + if ((u0 & 0xF0) == 0xE0) { + u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; + } else { + if ((u0 & 0xF8) != 0xF0) warnOnce(`Invalid UTF-8 leading byte ${ptrToString(u0)} encountered when deserializing a UTF-8 string in wasm memory to a JS string!`); + u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heapOrArray[idx++] & 63); + } + + if (u0 < 0x10000) { + str += String.fromCharCode(u0); + } else { + var ch = u0 - 0x10000; + str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); + } + } + return str; + }; + + /** + * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the + * emscripten HEAP, returns a copy of that string as a Javascript String object. + * + * @param {number} ptr + * @param {number=} maxBytesToRead - An optional length that specifies the + * maximum number of bytes to read. You can omit this parameter to scan the + * string until the first 0 byte. If maxBytesToRead is passed, and the string + * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the + * string will cut short at that byte index. + * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. + * @return {string} + */ + var UTF8ToString = (ptr, maxBytesToRead, ignoreNul) => { + assert(typeof ptr == 'number', `UTF8ToString expects a number (got ${typeof ptr})`); + return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead, ignoreNul) : ''; + }; + + /** + * @param {string|null=} returnType + * @param {Array=} argTypes + * @param {Array=} args + * @param {Object=} opts + */ + var ccall = (ident, returnType, argTypes, args, opts) => { + // For fast lookup of conversion functions + var toC = { + 'string': (str) => { + var ret = 0; + if (str !== null && str !== undefined && str !== 0) { // null string + ret = stringToUTF8OnStack(str); + } + return ret; + }, + 'array': (arr) => { + var ret = stackAlloc(arr.length); + writeArrayToMemory(arr, ret); + return ret; + } + }; + + function convertReturnValue(ret) { + if (returnType === 'string') { + return UTF8ToString(ret); + } + if (returnType === 'boolean') return Boolean(ret); + return ret; + } + + var func = getCFunc(ident); + var cArgs = []; + var stack = 0; + assert(returnType !== 'array', 'return type should not be "array"'); + if (args) { + for (var i = 0; i < args.length; i++) { + var converter = toC[argTypes[i]]; + if (converter) { + if (stack === 0) stack = stackSave(); + cArgs[i] = converter(args[i]); + } else { + cArgs[i] = args[i]; + } + } + } + var ret = func(...cArgs); + function onDone(ret) { + if (stack !== 0) stackRestore(stack); + return convertReturnValue(ret); + } + + ret = onDone(ret); + return ret; + }; +// End JS library code + +// include: postlibrary.js +// This file is included after the automatically-generated JS library code +// but before the wasm module is created. + +{ + + // Begin ATMODULES hooks + if (Module['noExitRuntime']) noExitRuntime = Module['noExitRuntime']; +if (Module['print']) out = Module['print']; +if (Module['printErr']) err = Module['printErr']; +if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; + +Module['FS_createDataFile'] = FS.createDataFile; +Module['FS_createPreloadedFile'] = FS.createPreloadedFile; + + // End ATMODULES hooks + + checkIncomingModuleAPI(); + + if (Module['arguments']) programArgs = Module['arguments']; + if (Module['thisProgram']) thisProgram = Module['thisProgram']; + + // Assertions on removed incoming Module JS APIs. + assert(typeof Module['memoryInitializerPrefixURL'] == 'undefined', 'Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['pthreadMainPrefixURL'] == 'undefined', 'Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['cdInitializerPrefixURL'] == 'undefined', 'Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['filePackagePrefixURL'] == 'undefined', 'Module.filePackagePrefixURL option was removed, use Module.locateFile instead'); + assert(typeof Module['read'] == 'undefined', 'Module.read option was removed'); + assert(typeof Module['readAsync'] == 'undefined', 'Module.readAsync option was removed (modify readAsync in JS)'); + assert(typeof Module['readBinary'] == 'undefined', 'Module.readBinary option was removed (modify readBinary in JS)'); + assert(typeof Module['setWindowTitle'] == 'undefined', 'Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)'); + assert(typeof Module['TOTAL_MEMORY'] == 'undefined', 'Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY'); + assert(typeof Module['ENVIRONMENT'] == 'undefined', 'Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)'); + assert(typeof Module['STACK_SIZE'] == 'undefined', 'STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time') + // If memory is defined in wasm, the user can't provide it, or set INITIAL_MEMORY + assert(typeof Module['wasmMemory'] == 'undefined', 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally'); + assert(typeof Module['INITIAL_MEMORY'] == 'undefined', 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically'); + + if (Module['preInit']) { + if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; + while (Module['preInit'].length > 0) { + Module['preInit'].shift()(); + } + } + consumedModuleProp('preInit'); +} + +// Begin runtime exports + Module['ccall'] = ccall; + var missingLibrarySymbols = [ + 'writeI53ToI64', + 'writeI53ToI64Clamped', + 'writeI53ToI64Signaling', + 'writeI53ToU64Clamped', + 'writeI53ToU64Signaling', + 'readI53FromI64', + 'readI53FromU64', + 'convertI32PairToI53', + 'convertI32PairToI53Checked', + 'convertU32PairToI53', + 'bigintToI53Checked', + 'getTempRet0', + 'setTempRet0', + 'createNamedFunction', + 'zeroMemory', + 'exitJS', + 'getHeapMax', + 'growMemory', + 'withStackSave', + 'strError', + 'inetPton4', + 'inetNtop4', + 'inetPton6', + 'inetNtop6', + 'readSockaddr', + 'writeSockaddr', + 'readEmAsmArgs', + 'jstoi_q', + 'getExecutableName', + 'autoResumeAudioContext', + 'getDynCaller', + 'dynCall', + 'handleException', + 'keepRuntimeAlive', + 'runtimeKeepalivePush', + 'runtimeKeepalivePop', + 'callUserCallback', + 'maybeExit', + 'asyncLoad', + 'asmjsMangle', + 'alignMemory', + 'mmapAlloc', + 'HandleAllocator', + 'getUniqueRunDependency', + 'addOnInit', + 'addOnPostCtor', + 'addOnPreMain', + 'addOnExit', + 'STACK_SIZE', + 'STACK_ALIGN', + 'POINTER_SIZE', + 'ASSERTIONS', + 'cwrap', + 'convertJsFunctionToWasm', + 'getEmptyTableSlot', + 'updateTableMap', + 'getFunctionAddress', + 'addFunction', + 'removeFunction', + 'intArrayFromString', + 'intArrayToString', + 'AsciiToString', + 'stringToAscii', + 'UTF16ToString', + 'stringToUTF16', + 'lengthBytesUTF16', + 'UTF32ToString', + 'stringToUTF32', + 'lengthBytesUTF32', + 'stringToNewUTF8', + 'registerKeyEventCallback', + 'maybeCStringToJsString', + 'findEventTarget', + 'getBoundingClientRect', + 'fillMouseEventData', + 'registerMouseEventCallback', + 'registerWheelEventCallback', + 'registerUiEventCallback', + 'registerFocusEventCallback', + 'fillDeviceOrientationEventData', + 'registerDeviceOrientationEventCallback', + 'fillDeviceMotionEventData', + 'registerDeviceMotionEventCallback', + 'screenOrientation', + 'fillOrientationChangeEventData', + 'registerOrientationChangeEventCallback', + 'fillFullscreenChangeEventData', + 'registerFullscreenChangeEventCallback', + 'JSEvents_requestFullscreen', + 'JSEvents_resizeCanvasForFullscreen', + 'registerRestoreOldStyle', + 'hideEverythingExceptGivenElement', + 'restoreHiddenElements', + 'setLetterbox', + 'softFullscreenResizeWebGLRenderTarget', + 'doRequestFullscreen', + 'fillPointerlockChangeEventData', + 'registerPointerlockChangeEventCallback', + 'registerPointerlockErrorEventCallback', + 'requestPointerLock', + 'fillVisibilityChangeEventData', + 'registerVisibilityChangeEventCallback', + 'registerTouchEventCallback', + 'fillGamepadEventData', + 'registerGamepadEventCallback', + 'registerBeforeUnloadEventCallback', + 'fillBatteryEventData', + 'registerBatteryEventCallback', + 'setCanvasElementSize', + 'getCanvasElementSize', + 'jsStackTrace', + 'getCallstack', + 'convertPCtoSourceLocation', + 'getEnvStrings', + 'checkWasiClock', + 'flush_NO_FILESYSTEM', + 'wasiRightsToMuslOFlags', + 'wasiOFlagsToMuslOFlags', + 'initRandomFill', + 'randomFill', + 'safeSetTimeout', + 'setImmediateWrapped', + 'safeRequestAnimationFrame', + 'clearImmediateWrapped', + 'registerPostMainLoop', + 'registerPreMainLoop', + 'getPromise', + 'makePromise', + 'addPromise', + 'idsToPromises', + 'makePromiseCallback', + 'ExceptionInfo', + 'findMatchingCatch', + 'incrementUncaughtExceptionCount', + 'decrementUncaughtExceptionCount', + 'Browser_asyncPrepareDataCounter', + 'isLeapYear', + 'ydayFromDate', + 'arraySum', + 'addDays', + 'getSocketFromFD', + 'getSocketAddress', + 'FS_createPreloadedFile', + 'FS_preloadFile', + 'FS_modeStringToFlags', + 'FS_getMode', + 'FS_fileDataToTypedArray', + 'FS_stdin_getChar', + 'FS_mkdirTree', + '_setNetworkCallback', + 'heapObjectForWebGLType', + 'toTypedArrayIndex', + 'webgl_enable_ANGLE_instanced_arrays', + 'webgl_enable_OES_vertex_array_object', + 'webgl_enable_WEBGL_draw_buffers', + 'webgl_enable_WEBGL_multi_draw', + 'webgl_enable_EXT_polygon_offset_clamp', + 'webgl_enable_EXT_clip_control', + 'webgl_enable_WEBGL_polygon_mode', + 'emscriptenWebGLGet', + 'computeUnpackAlignedImageSize', + 'colorChannelsInGlTextureFormat', + 'emscriptenWebGLGetTexPixelData', + 'emscriptenWebGLGetUniform', + 'webglGetProgramUniformLocation', + 'webglGetUniformLocation', + 'webglPrepareUniformLocationsBeforeFirstUse', + 'webglGetLeftBracePos', + 'emscriptenWebGLGetVertexAttrib', + '__glGetActiveAttribOrUniform', + 'writeGLArray', + 'registerWebGlEventCallback', + 'runAndAbortIfError', + 'ALLOC_NORMAL', + 'ALLOC_STACK', + 'allocate', + 'writeStringToMemory', + 'writeAsciiToMemory', + 'allocateUTF8', + 'allocateUTF8OnStack', + 'demangle', + 'stackTrace', + 'getNativeTypeSize', +]; +missingLibrarySymbols.forEach(missingLibrarySymbol) + + var unexportedSymbols = [ + 'run', + 'out', + 'err', + 'callMain', + 'abort', + 'wasmExports', + 'writeStackCookie', + 'checkStackCookie', + 'INT53_MAX', + 'INT53_MIN', + 'HEAP8', + 'HEAPU8', + 'HEAP16', + 'HEAPU16', + 'HEAP32', + 'HEAPU32', + 'HEAPF32', + 'HEAPF64', + 'HEAP64', + 'HEAPU64', + 'stackSave', + 'stackRestore', + 'stackAlloc', + 'ptrToString', + 'ENV', + 'ERRNO_CODES', + 'DNS', + 'Protocols', + 'Sockets', + 'timers', + 'warnOnce', + 'readEmAsmArgsArray', + 'wasmTable', + 'wasmMemory', + 'noExitRuntime', + 'addRunDependency', + 'removeRunDependency', + 'addOnPreRun', + 'addOnPostRun', + 'freeTableIndexes', + 'functionsInTableMap', + 'setValue', + 'getValue', + 'PATH', + 'PATH_FS', + 'UTF8Decoder', + 'UTF8ArrayToString', + 'UTF8ToString', + 'stringToUTF8Array', + 'stringToUTF8', + 'lengthBytesUTF8', + 'UTF16Decoder', + 'stringToUTF8OnStack', + 'writeArrayToMemory', + 'JSEvents', + 'specialHTMLTargets', + 'findCanvasEventTarget', + 'currentFullscreenStrategy', + 'restoreOldWindowedStyle', + 'UNWIND_CACHE', + 'ExitStatus', + 'emSetImmediate', + 'emClearImmediate_deps', + 'emClearImmediate', + 'promiseMap', + 'uncaughtExceptionCount', + 'exceptionCaught', + 'Browser', + 'requestFullscreen', + 'requestFullScreen', + 'setCanvasSize', + 'getUserMedia', + 'createContext', + 'getPreloadedImageData__data', + 'wget', + 'MONTH_DAYS_REGULAR', + 'MONTH_DAYS_LEAP', + 'MONTH_DAYS_REGULAR_CUMULATIVE', + 'MONTH_DAYS_LEAP_CUMULATIVE', + 'SYSCALLS', + 'preloadPlugins', + 'FS_stdin_getChar_buffer', + 'FS_unlink', + 'FS_createPath', + 'FS_createDevice', + 'FS_readFile', + 'FS', + 'FS_root', + 'FS_mounts', + 'FS_devices', + 'FS_streams', + 'FS_nextInode', + 'FS_nameTable', + 'FS_currentPath', + 'FS_initialized', + 'FS_ignorePermissions', + 'FS_filesystems', + 'FS_syncFSRequests', + 'FS_lookupPath', + 'FS_getPath', + 'FS_hashName', + 'FS_hashAddNode', + 'FS_hashRemoveNode', + 'FS_lookupNode', + 'FS_createNode', + 'FS_destroyNode', + 'FS_isRoot', + 'FS_isMountpoint', + 'FS_isFile', + 'FS_isDir', + 'FS_isLink', + 'FS_isChrdev', + 'FS_isBlkdev', + 'FS_isFIFO', + 'FS_isSocket', + 'FS_flagsToPermissionString', + 'FS_nodePermissions', + 'FS_mayLookup', + 'FS_mayCreate', + 'FS_mayDelete', + 'FS_mayOpen', + 'FS_checkOpExists', + 'FS_nextfd', + 'FS_getStreamChecked', + 'FS_getStream', + 'FS_createStream', + 'FS_closeStream', + 'FS_dupStream', + 'FS_doSetAttr', + 'FS_chrdev_stream_ops', + 'FS_major', + 'FS_minor', + 'FS_makedev', + 'FS_registerDevice', + 'FS_getDevice', + 'FS_getMounts', + 'FS_syncfs', + 'FS_mount', + 'FS_unmount', + 'FS_lookup', + 'FS_mknod', + 'FS_statfs', + 'FS_statfsStream', + 'FS_statfsNode', + 'FS_create', + 'FS_mkdir', + 'FS_mkdev', + 'FS_symlink', + 'FS_rename', + 'FS_rmdir', + 'FS_readdir', + 'FS_readlink', + 'FS_stat', + 'FS_fstat', + 'FS_lstat', + 'FS_doChmod', + 'FS_chmod', + 'FS_lchmod', + 'FS_fchmod', + 'FS_doChown', + 'FS_chown', + 'FS_lchown', + 'FS_fchown', + 'FS_doTruncate', + 'FS_truncate', + 'FS_ftruncate', + 'FS_utime', + 'FS_open', + 'FS_close', + 'FS_isClosed', + 'FS_llseek', + 'FS_read', + 'FS_write', + 'FS_mmap', + 'FS_msync', + 'FS_ioctl', + 'FS_writeFile', + 'FS_cwd', + 'FS_chdir', + 'FS_createDefaultDirectories', + 'FS_createDefaultDevices', + 'FS_createSpecialDirectories', + 'FS_createStandardStreams', + 'FS_staticInit', + 'FS_init', + 'FS_quit', + 'FS_findObject', + 'FS_analyzePath', + 'FS_createFile', + 'FS_createDataFile', + 'FS_forceLoadFile', + 'FS_createLazyFile', + 'MEMFS', + 'TTY', + 'PIPEFS', + 'SOCKFS', + 'tempFixedLengthArray', + 'miniTempWebGLFloatBuffers', + 'miniTempWebGLIntBuffers', + 'GL', + 'AL', + 'GLUT', + 'EGL', + 'GLEW', + 'IDBStore', + 'SDL', + 'SDL_gfx', + 'print', + 'printErr', + 'jstoi_s', +]; +unexportedSymbols.forEach(unexportedRuntimeSymbol); + + // End runtime exports + // Begin JS library exports + // End JS library exports + +// end include: postlibrary.js + +function checkIncomingModuleAPI() { + ignoredModuleProp('fetchSettings'); + ignoredModuleProp('logReadFiles'); + ignoredModuleProp('loadSplitModule'); + ignoredModuleProp('onMalloc'); + ignoredModuleProp('onRealloc'); + ignoredModuleProp('onFree'); + ignoredModuleProp('onSbrkGrow'); +} + +// Imports from the Wasm binary. +var _greet = Module['_greet'] = makeInvalidEarlyAccess('_greet'); +var _fflush = makeInvalidEarlyAccess('_fflush'); +var _emscripten_stack_init = makeInvalidEarlyAccess('_emscripten_stack_init'); +var _emscripten_stack_get_free = makeInvalidEarlyAccess('_emscripten_stack_get_free'); +var _emscripten_stack_get_base = makeInvalidEarlyAccess('_emscripten_stack_get_base'); +var _emscripten_stack_get_end = makeInvalidEarlyAccess('_emscripten_stack_get_end'); +var __emscripten_stack_restore = makeInvalidEarlyAccess('__emscripten_stack_restore'); +var __emscripten_stack_alloc = makeInvalidEarlyAccess('__emscripten_stack_alloc'); +var _emscripten_stack_get_current = makeInvalidEarlyAccess('_emscripten_stack_get_current'); +var memory = makeInvalidEarlyAccess('memory'); +var __indirect_function_table = makeInvalidEarlyAccess('__indirect_function_table'); +var wasmMemory = makeInvalidEarlyAccess('wasmMemory'); + +function assignWasmExports(wasmExports) { + assert(typeof wasmExports['greet'] != 'undefined', 'missing Wasm export: greet'); + assert(typeof wasmExports['fflush'] != 'undefined', 'missing Wasm export: fflush'); + assert(typeof wasmExports['emscripten_stack_init'] != 'undefined', 'missing Wasm export: emscripten_stack_init'); + assert(typeof wasmExports['emscripten_stack_get_free'] != 'undefined', 'missing Wasm export: emscripten_stack_get_free'); + assert(typeof wasmExports['emscripten_stack_get_base'] != 'undefined', 'missing Wasm export: emscripten_stack_get_base'); + assert(typeof wasmExports['emscripten_stack_get_end'] != 'undefined', 'missing Wasm export: emscripten_stack_get_end'); + assert(typeof wasmExports['_emscripten_stack_restore'] != 'undefined', 'missing Wasm export: _emscripten_stack_restore'); + assert(typeof wasmExports['_emscripten_stack_alloc'] != 'undefined', 'missing Wasm export: _emscripten_stack_alloc'); + assert(typeof wasmExports['emscripten_stack_get_current'] != 'undefined', 'missing Wasm export: emscripten_stack_get_current'); + assert(typeof wasmExports['memory'] != 'undefined', 'missing Wasm export: memory'); + assert(typeof wasmExports['__indirect_function_table'] != 'undefined', 'missing Wasm export: __indirect_function_table'); + _greet = Module['_greet'] = createExportWrapper('greet', 0); + _fflush = createExportWrapper('fflush', 1); + _emscripten_stack_init = wasmExports['emscripten_stack_init']; + _emscripten_stack_get_free = wasmExports['emscripten_stack_get_free']; + _emscripten_stack_get_base = wasmExports['emscripten_stack_get_base']; + _emscripten_stack_get_end = wasmExports['emscripten_stack_get_end']; + __emscripten_stack_restore = wasmExports['_emscripten_stack_restore']; + __emscripten_stack_alloc = wasmExports['_emscripten_stack_alloc']; + _emscripten_stack_get_current = wasmExports['emscripten_stack_get_current']; + memory = wasmMemory = wasmExports['memory']; + __indirect_function_table = wasmExports['__indirect_function_table']; +} + +var wasmImports = { + +}; + + +// include: postamble.js +// === Auto-generated postamble setup entry stuff === + +var calledRun; + +function stackCheckInit() { + // This is normally called automatically during __wasm_call_ctors but need to + // get these values before even running any of the ctors so we call it redundantly + // here. + _emscripten_stack_init(); + // TODO(sbc): Move writeStackCookie to native to to avoid this. + writeStackCookie(); +} + +function run() { + + if (runDependencies > 0) { + dependenciesFulfilled = run; + return; + } + + stackCheckInit(); + + preRun(); + + // a preRun added a dependency, run will be called later + if (runDependencies > 0) { + dependenciesFulfilled = run; + return; + } + + function doRun() { + // run may have just been called through dependencies being fulfilled just in this very frame, + // or while the async setStatus time below was happening + assert(!calledRun); + calledRun = true; + Module['calledRun'] = true; + + if (ABORT) return; + + initRuntime(); + + Module['onRuntimeInitialized']?.(); + consumedModuleProp('onRuntimeInitialized'); + + assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'); + + postRun(); + } + + if (Module['setStatus']) { + Module['setStatus']('Running...'); + setTimeout(() => { + setTimeout(() => Module['setStatus'](''), 1); + doRun(); + }, 1); + } else + { + doRun(); + } + checkStackCookie(); +} + +function checkUnflushedContent() { + // Compiler settings do not allow exiting the runtime, so flushing + // the streams is not possible. but in ASSERTIONS mode we check + // if there was something to flush, and if so tell the user they + // should request that the runtime be exitable. + // Normally we would not even include flush() at all, but in ASSERTIONS + // builds we do so just for this check, and here we see if there is any + // content to flush, that is, we check if there would have been + // something a non-ASSERTIONS build would have not seen. + // How we flush the streams depends on whether we are in SYSCALLS_REQUIRE_FILESYSTEM=0 + // mode (which has its own special function for this; otherwise, all + // the code is inside libc) + var oldOut = out; + var oldErr = err; + var has = false; + out = err = (x) => { + has = true; + } + try { // it doesn't matter if it fails + _fflush(0); + } catch(e) {} + out = oldOut; + err = oldErr; + if (has) { + warnOnce('stdio streams had content in them that was not flushed. you should set EXIT_RUNTIME to 1 (see the Emscripten FAQ), or make sure to emit a newline when you printf etc.'); + warnOnce('(this may also be due to not including full filesystem support - try building with -sFORCE_FILESYSTEM)'); + } +} + +var wasmExports; + +// With async instantation wasmExports is assigned asynchronously when the +// instance is received. +createWasm(); + +run(); + +// end include: postamble.js + diff --git a/test/cross_origin_storage/index.wasm b/test/cross_origin_storage/index.wasm new file mode 100755 index 0000000000000000000000000000000000000000..a7b17d387c1852563c9f194925da15d7892a7698 GIT binary patch literal 994 zcmaJ=&2H2%5T3EK+hyH0TPV<0m2kIvSSiXCI5tQi-hgA8O}$Il{MB&^y)>ah{2w`R z;J^d$1UwzK(@Kb~kP>_R%{SjnCS!=I3;_Ti^p9!NG}!3vqQQ$}fMd`nY(T%*PkmtE9+770ptC(4WuKx_Sx2yCSP%nUqT8VWpyYsbdla zWOoH$2rZ+Pf!^$!MU?<&=f*gZpFnRnT=7X!>6!(w`>Q_vMMVtj`zt}Fkhqpo|D@mQy4Z1l;*addT>eH-cOl%sCoGgC@7MpI_&UAsM#qf~qb9e%e=*Vuw(E~cvNhX%V_HV`9+-j0W_7ysyDQ+i ze;xdyRNg&v7yRqV_lq+j(;{DZ!>mqKau%hv@Z7An7r|eo#VkrI?@nIiN=9Xw Date: Tue, 9 Jun 2026 17:20:56 +0200 Subject: [PATCH 44/87] =?UTF-8?q?fix:=20CI=20failures=20=E2=80=94=20ruff,?= =?UTF-8?q?=20build-docs,=20and=20browser-test=20experimental=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test/setup_cos_extension.py: use r""" because the docstring contains backslash-continuation sequences (ruff D301) - test/test_browser.py: add trailing comma in skipTest string concat (ruff COM812); add -Wno-experimental to both COS browser tests so that the new EXPERIMENTAL_SETTINGS warning does not become an error under the browser test runner's implicit -Werror - src/settings.js: move [experimental] onto its own comment line for CROSS_ORIGIN_STORAGE so that update_settings_docs.py recognises it as a tag (the parser requires the entire line to be just [tag]) - site/source/docs/tools_reference/settings_reference.rst: regenerated via tools/maint/update_settings_docs.py to pick up the tag change --- site/source/docs/tools_reference/settings_reference.rst | 4 +++- src/settings.js | 3 ++- test/setup_cos_extension.py | 2 +- test/test_browser.py | 5 +++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 2940422148e77..cf6f68af7fcec 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3365,7 +3365,7 @@ Default value: false CROSS_ORIGIN_STORAGE ==================== -[experimental] Enables Cross-Origin Storage (COS) API support for Wasm +Enables Cross-Origin Storage (COS) API support for Wasm loading on the Web target. At link time Emscripten computes the SHA-256 hash of the final ``.wasm`` binary and embeds it in the generated JS. At runtime the COS API is used as a progressive enhancement: the binary is @@ -3378,6 +3378,8 @@ WASM_ASYNC_COMPILATION=0 (both produce hard link-time errors). See :ref:`CrossOriginStorage` for the full guide. +.. note:: This is an experimental setting + Default value: 0 .. _cross_origin_storage_origins: diff --git a/src/settings.js b/src/settings.js index 77e3e6044c6ba..d329d4c4d8bcb 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2205,7 +2205,8 @@ var GROWABLE_ARRAYBUFFERS = false; // indirectly using `importScripts` var CROSS_ORIGIN = false; -// [experimental] Enables Cross-Origin Storage (COS) API support for Wasm +// [experimental] +// Enables Cross-Origin Storage (COS) API support for Wasm // loading on the Web target. At link time Emscripten computes the SHA-256 // hash of the final ``.wasm`` binary and embeds it in the generated JS. // At runtime the COS API is used as a progressive enhancement: the binary is diff --git a/test/setup_cos_extension.py b/test/setup_cos_extension.py index 8e7ed11ee0c36..970ffdf40b7c1 100755 --- a/test/setup_cos_extension.py +++ b/test/setup_cos_extension.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: Apache-2.0 -"""Download the Cross-Origin Storage Chrome extension for COS browser tests. +r"""Download the Cross-Origin Storage Chrome extension for COS browser tests. The COS extension polyfills navigator.crossOriginStorage in Chrome so that automated browser tests can exercise the cache-miss and cache-hit paths without diff --git a/test/test_browser.py b/test/test_browser.py index e7713b159fa98..162e7b14c229f 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5709,7 +5709,7 @@ def test_cross_origin_storage_fallback(self): if not is_chrome(): self.skipTest('cross-origin storage tests require a Chromium-based browser') self.btest_exit('browser_test_hello_world.c', - cflags=['-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web']) + cflags=['-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-Wno-experimental']) def test_cross_origin_storage_miss_then_hit(self): """COS flag: first load triggers a cache-miss store; second load is a hit. @@ -5727,7 +5727,7 @@ def test_cross_origin_storage_miss_then_hit(self): 'set EMTEST_COS_EXTENSION_PATH to the COS extension directory; ' 'run test/setup_cos_extension.py to download it automatically. ' 'Note: --load-extension requires Chromium or Chrome for Testing, ' - 'not the official Google Chrome release.' + 'not the official Google Chrome release.', ) # Restart the browser with a fresh user-data-dir so the extension starts @@ -5752,6 +5752,7 @@ def test_cross_origin_storage_miss_then_hit(self): self.compile_btest('browser_test_hello_world.c', [ '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', + '-Wno-experimental', '-sINCOMING_MODULE_JS_API=onAbort,onExit,onCOSStore,onCOSCacheHit', '--pre-js', 'cos_pre.js', '-o', 'page.html', From 74c005ab29a544171e4dc2a98f98d68c828a54b7 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 17:36:31 +0200 Subject: [PATCH 45/87] remove: dead WASM_SHA256 internal setting The setting was never wired up: it was always '' and no JS glue template ever referenced it with {{{ WASM_SHA256 }}}. The actual hash injection uses the <<< WASM_HASH_ALGORITHM >>> / <<< WASM_HASH_VALUE >>> late- replacement placeholders substituted by do_replace() in tools/link.py. --- src/settings_internal.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/settings_internal.js b/src/settings_internal.js index 482943d54f7bf..ef72ff8a4789c 100644 --- a/src/settings_internal.js +++ b/src/settings_internal.js @@ -127,11 +127,6 @@ var USER_EXPORTS = []; // name of the file containing wasm binary, if relevant var WASM_BINARY_FILE = ''; -// SHA-256 hex digest of the final .wasm binary, computed at link time. -// Populated automatically by tools/link.py when CROSS_ORIGIN_STORAGE=1. -// Available in JS glue templates as {{{ WASM_SHA256 }}}. -var WASM_SHA256 = ''; - // Base URL the source mapfile, if relevant var SOURCE_MAP_BASE = ''; From 8b23b33977ec7c5c69f3c6025f8ae9eaf79d9693 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 17:45:19 +0200 Subject: [PATCH 46/87] style: add -O2 to COS example build and browser tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without an optimisation flag the JS output is ~60 KB; with -O2 it drops to ~10 KB. Other browser tests in the suite pass -O2 (or higher) explicitly — follow the same convention. --- test/cross_origin_storage/Makefile | 1 + test/cross_origin_storage/README.md | 1 + test/cross_origin_storage/main.cpp | 1 + test/test_browser.py | 3 ++- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/cross_origin_storage/Makefile b/test/cross_origin_storage/Makefile index 00d75aaee3a15..024872280c5e6 100644 --- a/test/cross_origin_storage/Makefile +++ b/test/cross_origin_storage/Makefile @@ -11,6 +11,7 @@ all: index.js index.js: main.cpp $(EMCC) main.cpp -o index.js \ + -O2 \ -sCROSS_ORIGIN_STORAGE \ -sENVIRONMENT=web \ -sEXPORTED_RUNTIME_METHODS=ccall \ diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md index 5e8ecf1a24135..b289998c20a46 100644 --- a/test/cross_origin_storage/README.md +++ b/test/cross_origin_storage/README.md @@ -26,6 +26,7 @@ of the Wasm resource and the URL it was fetched from. ```bash emcc main.cpp -o index.js \ + -O2 \ -sCROSS_ORIGIN_STORAGE \ -sENVIRONMENT=web \ -sEXPORTED_RUNTIME_METHODS=ccall \ diff --git a/test/cross_origin_storage/main.cpp b/test/cross_origin_storage/main.cpp index cade88d44ef37..946e88d7d9ed0 100644 --- a/test/cross_origin_storage/main.cpp +++ b/test/cross_origin_storage/main.cpp @@ -7,6 +7,7 @@ // // Build with: // emcc main.cpp -o index.js \ +// -O2 \ // -sCROSS_ORIGIN_STORAGE \ // -sENVIRONMENT=web \ // -sEXPORTED_RUNTIME_METHODS=ccall \ diff --git a/test/test_browser.py b/test/test_browser.py index 162e7b14c229f..f38e57db9f0f5 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5709,7 +5709,7 @@ def test_cross_origin_storage_fallback(self): if not is_chrome(): self.skipTest('cross-origin storage tests require a Chromium-based browser') self.btest_exit('browser_test_hello_world.c', - cflags=['-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-Wno-experimental']) + cflags=['-O2', '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-Wno-experimental']) def test_cross_origin_storage_miss_then_hit(self): """COS flag: first load triggers a cache-miss store; second load is a hit. @@ -5750,6 +5750,7 @@ def test_cross_origin_storage_miss_then_hit(self): }; ''') self.compile_btest('browser_test_hello_world.c', [ + '-O2', '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-Wno-experimental', From 3f808be73ad65942a88cce9e1e65f07cd899c25a Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 9 Jun 2026 17:50:09 +0200 Subject: [PATCH 47/87] chore: untrack generated COS example build artifacts index.js and index.wasm are emcc outputs, not source files. Add them to .gitignore and remove them from the index. --- .gitignore | 4 + test/cross_origin_storage/index.js | 1759 -------------------------- test/cross_origin_storage/index.wasm | Bin 994 -> 0 bytes 3 files changed, 4 insertions(+), 1759 deletions(-) delete mode 100644 test/cross_origin_storage/index.js delete mode 100755 test/cross_origin_storage/index.wasm diff --git a/.gitignore b/.gitignore index 234517cc97797..33e2179a20b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,10 @@ coverage.xml # Test output /out/ +# COS example build artifacts +/test/cross_origin_storage/index.js +/test/cross_origin_storage/index.wasm + # When updating the website we check it out here. /site/emscripten-site/ diff --git a/test/cross_origin_storage/index.js b/test/cross_origin_storage/index.js deleted file mode 100644 index 890b272da8989..0000000000000 --- a/test/cross_origin_storage/index.js +++ /dev/null @@ -1,1759 +0,0 @@ -// include: shell.js -// include: minimum_runtime_check.js -(function() { - // "30.0.0" -> 300000 - function humanReadableVersionToPacked(str) { - str = str.split('-')[0]; // Remove any trailing part from e.g. "12.53.3-alpha" - var vers = str.split('.').slice(0, 3); - while(vers.length < 3) vers.push('00'); - vers = vers.map((n, i, arr) => n.padStart(2, '0')); - return vers.join(''); - } - // 300000 -> "30.0.0" - var packedVersionToHumanReadable = n => [n / 10000 | 0, (n / 100 | 0) % 100, n % 100].join('.'); - - var TARGET_NOT_SUPPORTED = 2147483647; - - // Note: We use a typeof check here instead of optional chaining using - // globalThis because older browsers might not have globalThis defined. - var currentNodeVersion = typeof process !== 'undefined' && process.versions?.node ? humanReadableVersionToPacked(process.versions.node) : TARGET_NOT_SUPPORTED; - if (currentNodeVersion < TARGET_NOT_SUPPORTED) { - throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - } - if (currentNodeVersion < 2147483647) { - throw new Error(`This emscripten-generated code requires node v${ packedVersionToHumanReadable(2147483647) } (detected v${packedVersionToHumanReadable(currentNodeVersion)})`); - } - - var userAgent = typeof navigator !== 'undefined' && navigator.userAgent; - if (!userAgent) { - return; - } - - var currentSafariVersion = userAgent.includes("Safari/") && !userAgent.includes("Chrome/") && userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/) ? humanReadableVersionToPacked(userAgent.match(/Version\/(\d+\.?\d*\.?\d*)/)[1]) : TARGET_NOT_SUPPORTED; - if (currentSafariVersion < 150000) { - throw new Error(`This emscripten-generated code requires Safari v${ packedVersionToHumanReadable(150000) } (detected v${currentSafariVersion})`); - } - - var currentFirefoxVersion = userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Firefox\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; - if (currentFirefoxVersion < 79) { - throw new Error(`This emscripten-generated code requires Firefox v79 (detected v${currentFirefoxVersion})`); - } - - var currentChromeVersion = userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/) ? parseFloat(userAgent.match(/Chrome\/(\d+(?:\.\d+)?)/)[1]) : TARGET_NOT_SUPPORTED; - if (currentChromeVersion < 85) { - throw new Error(`This emscripten-generated code requires Chrome v85 (detected v${currentChromeVersion})`); - } -})(); - -// end include: minimum_runtime_check.js -// The Module object: Our interface to the outside world. We import -// and export values on it. There are various ways Module can be used: -// 1. Not defined. We create it here -// 2. A function parameter, function(moduleArg) => Promise -// 3. pre-run appended it, var Module = {}; ..generated code.. -// 4. External script tag defines var Module. -// We need to check if Module already exists (e.g. case 3 above). -// Substitution will be replaced with actual code on later stage of the build, -// this way Closure Compiler will not mangle it (e.g. case 4. above). -// Note that if you want to run closure, and also to use Module -// after the generated code, you will need to define var Module = {}; -// before the code. Then that object will be used in the code, and you -// can continue to use Module afterwards as well. -var Module = typeof Module != 'undefined' ? Module : {}; - -// Determine the runtime environment we are in. You can customize this by -// setting the ENVIRONMENT setting at compile time (see settings.js). - -// Attempt to auto-detect the environment -var ENVIRONMENT_IS_WEB = !!globalThis.window; -var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; -// N.b. Electron.js environment is simultaneously a NODE-environment, but -// also a web environment. -var ENVIRONMENT_IS_NODE = globalThis.process?.versions?.node && globalThis.process?.type != 'renderer'; -var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; - -// --pre-jses are emitted after the Module integration code, so that they can -// refer to Module (if they choose; they can also define Module) - - -var programArgs = []; -var thisProgram = './this.program'; -var quit_ = (status, toThrow) => { - throw toThrow; -}; - -// In MODULARIZE mode _scriptName needs to be captured already at the very top of the page immediately when the page is parsed, so it is generated there -// before the page load. In non-MODULARIZE modes generate it here. -var _scriptName = globalThis.document?.currentScript?.src; - -// `/` should be present at the end if `scriptDirectory` is not empty -var scriptDirectory = ''; -function locateFile(path) { - if (Module['locateFile']) { - return Module['locateFile'](path, scriptDirectory); - } - return scriptDirectory + path; -} - -// Hooks that are implemented differently in different runtime environments. -var readAsync, readBinary; - -if (ENVIRONMENT_IS_SHELL) { - -} else - -// Note that this includes Node.js workers when relevant (pthreads is enabled). -// Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and -// ENVIRONMENT_IS_NODE. -if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { - try { - scriptDirectory = new URL('.', _scriptName).href; // includes trailing slash - } catch { - // Must be a `blob:` or `data:` URL (e.g. `blob:http://site.com/etc/etc`), we cannot - // infer anything from them. - } - - if (!(globalThis.window || globalThis.WorkerGlobalScope)) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - - { -// include: web_or_worker_shell_read.js -readAsync = async (url) => { - assert(!isFileURI(url), "readAsync does not work with file:// URLs"); - var response = await fetch(url, { credentials: 'same-origin' }); - if (response.ok) { - return response.arrayBuffer(); - } - throw new Error(response.status + ' : ' + response.url); - }; -// end include: web_or_worker_shell_read.js - } -} else -{ - throw new Error('environment detection error'); -} - -var out = console.log.bind(console); -var err = console.error.bind(console); - -var IDBFS = 'IDBFS is no longer included by default; build with -lidbfs.js'; -var PROXYFS = 'PROXYFS is no longer included by default; build with -lproxyfs.js'; -var WORKERFS = 'WORKERFS is no longer included by default; build with -lworkerfs.js'; -var FETCHFS = 'FETCHFS is no longer included by default; build with -lfetchfs.js'; -var ICASEFS = 'ICASEFS is no longer included by default; build with -licasefs.js'; -var JSFILEFS = 'JSFILEFS is no longer included by default; build with -ljsfilefs.js'; -var OPFS = 'OPFS is no longer included by default; build with -lopfs.js'; - -var NODEFS = 'NODEFS is no longer included by default; build with -lnodefs.js'; - -// perform assertions in shell.js after we set up out() and err(), as otherwise -// if an assertion fails it cannot print the message - -assert(!ENVIRONMENT_IS_WORKER, 'worker environment detected but not enabled at build time (add `worker` to `-sENVIRONMENT` to enable)'); - -assert(!ENVIRONMENT_IS_NODE, 'node environment detected but not enabled at build time (add `node` to `-sENVIRONMENT` to enable)'); - -assert(!ENVIRONMENT_IS_SHELL, 'shell environment detected but not enabled at build time (add `shell` to `-sENVIRONMENT` to enable)'); - -// end include: shell.js - -// include: preamble.js -// === Preamble library stuff === - -// Documentation for the public APIs defined in this file must be updated in: -// site/source/docs/api_reference/preamble.js.rst -// A prebuilt local version of the documentation is available at: -// site/build/text/docs/api_reference/preamble.js.txt -// You can also build docs locally as HTML or other formats in site/ -// An online HTML version (which may be of a different version of Emscripten) -// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html - -var wasmBinary; - -if (!globalThis.WebAssembly) { - err('no native wasm support detected'); -} - -// Wasm globals - -//======================================== -// Runtime essentials -//======================================== - -// whether we are quitting the application. no code should run after this. -// set in exit() and abort() -var ABORT = false; - -// set by exit() and abort(). Passed to 'onExit' handler. -// NOTE: This is also used as the process return code in shell environments -// but only when noExitRuntime is false. -var EXITSTATUS; - -// In STRICT mode, we only define assert() when ASSERTIONS is set. i.e. we -// don't define it at all in release modes. This matches the behaviour of -// MINIMAL_RUNTIME. -// TODO(sbc): Make this the default even without STRICT enabled. -/** @type {function(*, string=)} */ -function assert(condition, text) { - if (!condition) { - abort('Assertion failed' + (text ? ': ' + text : '')); - } -} - -// We used to include malloc/free by default in the past. Show a helpful error in -// builds with assertions. -function _malloc() { - abort('malloc() called but not included in the build - add `_malloc` to EXPORTED_FUNCTIONS'); -} -function _free() { - // Show a helpful error since we used to include free by default in the past. - abort('free() called but not included in the build - add `_free` to EXPORTED_FUNCTIONS'); -} - -/** - * Indicates whether filename is delivered via file protocol (as opposed to http/https) - * @noinline - */ -var isFileURI = (filename) => filename.startsWith('file://'); - -// include: runtime_common.js -// include: runtime_stack_check.js -// Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode. -function writeStackCookie() { - var max = _emscripten_stack_get_end(); - assert((max & 3) == 0); - // If the stack ends at address zero we write our cookies 4 bytes into the - // stack. This prevents interference with SAFE_HEAP and ASAN which also - // monitor writes to address zero. - if (max == 0) { - max += 4; - } - // The stack grow downwards towards _emscripten_stack_get_end. - // We write cookies to the final two words in the stack and detect if they are - // ever overwritten. - HEAPU32[((max)>>2)] = 0x02135467; - HEAPU32[(((max)+(4))>>2)] = 0x89BACDFE; - // Also test the global address 0 for integrity. - HEAPU32[((0)>>2)] = 1668509029; -} - -function checkStackCookie() { - if (ABORT) return; - var max = _emscripten_stack_get_end(); - // See writeStackCookie(). - if (max == 0) { - max += 4; - } - var cookie1 = HEAPU32[((max)>>2)]; - var cookie2 = HEAPU32[(((max)+(4))>>2)]; - if (cookie1 != 0x02135467 || cookie2 != 0x89BACDFE) { - abort(`Stack overflow! Stack cookie has been overwritten at ${ptrToString(max)}, expected hex dwords 0x89BACDFE and 0x2135467, but received ${ptrToString(cookie2)} ${ptrToString(cookie1)}`); - } - // Also test the global address 0 for integrity. - if (HEAPU32[((0)>>2)] != 0x63736d65 /* 'emsc' */) { - abort('Runtime error: The application has corrupted its heap memory area (address zero)!'); - } -} -// end include: runtime_stack_check.js -// include: runtime_exceptions.js -// Base Emscripten EH error class -class EmscriptenEH {} - -class EmscriptenSjLj extends EmscriptenEH {} - -// end include: runtime_exceptions.js -// include: runtime_debug.js -var runtimeDebug = true; // Switch to false at runtime to disable logging at the right times - -// Used by XXXXX_DEBUG settings to output debug messages. -function dbg(...args) { - if (!runtimeDebug && typeof runtimeDebug != 'undefined') return; - // TODO(sbc): Make this configurable somehow. Its not always convenient for - // logging to show up as warnings. - console.warn(...args); -} - -// Endianness check -(() => { - var h16 = new Int16Array(1); - var h8 = new Int8Array(h16.buffer); - h16[0] = 0x6373; - if (h8[0] !== 0x73 || h8[1] !== 0x63) abort('Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)'); -})(); - -function consumedModuleProp(prop) { - if (!Object.getOwnPropertyDescriptor(Module, prop)) { - Object.defineProperty(Module, prop, { - configurable: true, - set() { - abort(`Attempt to set \`Module.${prop}\` after it has already been processed. This can happen, for example, when code is injected via '--post-js' rather than '--pre-js'`); - - } - }); - } -} - -function makeInvalidEarlyAccess(name) { - return () => assert(false, `call to '${name}' via reference taken before Wasm module initialization`); - -} - -function ignoredModuleProp(prop) { - if (Object.getOwnPropertyDescriptor(Module, prop)) { - abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`); - } -} - -// forcing the filesystem exports a few things by default -function isExportedByForceFilesystem(name) { - return name === 'FS_createPath' || - name === 'FS_createDataFile' || - name === 'FS_createPreloadedFile' || - name === 'FS_preloadFile' || - name === 'FS_unlink' || - name === 'addRunDependency' || - // The old FS has some functionality that WasmFS lacks. - name === 'FS_createLazyFile' || - name === 'FS_createDevice' || - name === 'removeRunDependency'; -} - -/** - * Intercept access to a symbols in the global symbol. This enables us to give - * informative warnings/errors when folks attempt to use symbols they did not - * include in their build, or no symbols that no longer exist. - * - * We don't define this in MODULARIZE mode since in that mode emscripten symbols - * are never placed in the global scope. - */ -function hookGlobalSymbolAccess(sym, func) { - if (!Object.getOwnPropertyDescriptor(globalThis, sym)) { - Object.defineProperty(globalThis, sym, { - configurable: true, - get() { - func(); - return undefined; - } - }); - } -} - -function missingGlobal(sym, msg) { - hookGlobalSymbolAccess(sym, () => { - warnOnce(`\`${sym}\` is no longer defined by emscripten. ${msg}`); - }); -} - -missingGlobal('buffer', 'Please use HEAP8.buffer or wasmMemory.buffer'); -missingGlobal('asm', 'Please use wasmExports instead'); - -function missingLibrarySymbol(sym) { - hookGlobalSymbolAccess(sym, () => { - // Can't `abort()` here because it would break code that does runtime - // checks. e.g. `if (typeof SDL === 'undefined')`. - var msg = `\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`; - // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE requires the name as it appears in - // library.js, which means $name for a JS name with no prefix, or name - // for a JS name like _name. - var librarySymbol = sym; - if (!librarySymbol.startsWith('_')) { - librarySymbol = '$' + sym; - } - msg += ` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`; - if (isExportedByForceFilesystem(sym)) { - msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; - } - warnOnce(msg); - }); - - // Any symbol that is not included from the JS library is also (by definition) - // not exported on the Module object. - unexportedRuntimeSymbol(sym); -} - -function unexportedRuntimeSymbol(sym) { - if (!Object.getOwnPropertyDescriptor(Module, sym)) { - Object.defineProperty(Module, sym, { - configurable: true, - get() { - var msg = `'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`; - if (isExportedByForceFilesystem(sym)) { - msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; - } - abort(msg); - }, - }); - } -} - -// end include: runtime_debug.js -// Memory management - -var runtimeInitialized = false; - - - -function updateMemoryViews() { - var b = wasmMemory.buffer; - HEAP8 = new Int8Array(b); - HEAP16 = new Int16Array(b); - HEAPU8 = new Uint8Array(b); - HEAPU16 = new Uint16Array(b); - HEAP32 = new Int32Array(b); - HEAPU32 = new Uint32Array(b); - HEAPF32 = new Float32Array(b); - HEAPF64 = new Float64Array(b); - HEAP64 = new BigInt64Array(b); - HEAPU64 = new BigUint64Array(b); -} - -// include: memoryprofiler.js -// end include: memoryprofiler.js -// end include: runtime_common.js -assert(globalThis.Int32Array && globalThis.Float64Array && Int32Array.prototype.subarray && Int32Array.prototype.set, - 'JS engine does not provide full typed array support'); - -function preRun() { - if (Module['preRun']) { - if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; - while (Module['preRun'].length) { - addOnPreRun(Module['preRun'].shift()); - } - } - consumedModuleProp('preRun'); - // Begin ATPRERUNS hooks - callRuntimeCallbacks(onPreRuns); - // End ATPRERUNS hooks -} - -function initRuntime() { - assert(!runtimeInitialized); - runtimeInitialized = true; - - checkStackCookie(); - - // No ATINITS hooks - - wasmExports['__wasm_call_ctors'](); - - // No ATPOSTCTORS hooks -} - -function postRun() { - checkStackCookie(); - // PThreads reuse the runtime from the main thread. - - if (Module['postRun']) { - if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; - while (Module['postRun'].length) { - addOnPostRun(Module['postRun'].shift()); - } - } - consumedModuleProp('postRun'); - - // Begin ATPOSTRUNS hooks - callRuntimeCallbacks(onPostRuns); - // End ATPOSTRUNS hooks -} - -/** - * @param {string|number=} what - */ -function abort(what) { - Module['onAbort']?.(what); - - what = `Aborted(${what})`; - // TODO(sbc): Should we remove printing and leave it up to whoever - // catches the exception? - err(what); - - ABORT = true; - - // Use a wasm runtime error, because a JS error might be seen as a foreign - // exception, which means we'd run destructors on it. We need the error to - // simply make the program stop. - // FIXME This approach does not work in Wasm EH because it currently does not assume - // all RuntimeErrors are from traps; it decides whether a RuntimeError is from - // a trap or not based on a hidden field within the object. So at the moment - // we don't have a way of throwing a wasm trap from JS. TODO Make a JS API that - // allows this in the wasm spec. - - // Suppress closure compiler warning here. Closure compiler's builtin extern - // definition for WebAssembly.RuntimeError claims it takes no arguments even - // though it can. - // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure gets fixed. - /** @suppress {checkTypes} */ - var e = new WebAssembly.RuntimeError(what); - - // Throw the error whether or not MODULARIZE is set because abort is used - // in code paths apart from instantiation where an exception is expected - // to be thrown when abort is called. - throw e; -} - -// show errors on likely calls to FS when it was not included -function fsMissing() { - abort('Filesystem support (FS) was not included. The problem is that you are using files from JS, but files were not used from C/C++, so filesystem support was not auto-included. You can force-include filesystem support with -sFORCE_FILESYSTEM'); -} -var FS = { - init: fsMissing, - createDataFile: fsMissing, - createPreloadedFile: fsMissing, - createLazyFile: fsMissing, - open: fsMissing, - mkdev: fsMissing, - registerDevice: fsMissing, - analyzePath: fsMissing, - ErrnoError: fsMissing, -}; - - -function createExportWrapper(name, nargs) { - return (...args) => { - assert(runtimeInitialized, `native function \`${name}\` called before runtime initialization`); - var f = wasmExports[name]; - assert(f, `exported native function \`${name}\` not found`); - // Only assert for too many arguments. Too few can be valid since the missing arguments will be zero filled. - assert(args.length <= nargs, `native function \`${name}\` called with ${args.length} args but expects ${nargs}`); - return f(...args); - }; -} - -var wasmBinaryFile; - -function findWasmBinary() { - return locateFile('index.wasm'); -} - -function getBinarySync(file) { - if (file == wasmBinaryFile && wasmBinary) { - return new Uint8Array(wasmBinary); - } - if (readBinary) { - return readBinary(file); - } - // Throwing a plain string here, even though it not normally advisable since - // this gets turning into an `abort` in instantiateArrayBuffer. - throw 'both async and sync fetching of the wasm failed'; -} - -async function getWasmBinary(binaryFile) { - // If we don't have the binary yet, load it asynchronously using readAsync. - if (!wasmBinary) { - // Fetch the binary using readAsync - try { - var response = await readAsync(binaryFile); - return new Uint8Array(response); - } catch { - // Fall back to getBinarySync below; - } - } - - // Otherwise, getBinarySync should be able to get it synchronously - return getBinarySync(binaryFile); -} - -async function instantiateArrayBuffer(binaryFile, imports) { - try { - var binary = await getWasmBinary(binaryFile); - var instance = await WebAssembly.instantiate(binary, imports); - return instance; - } catch (reason) { - err(`failed to asynchronously prepare wasm: ${reason}`); - - // Warn on some common problems. - if (isFileURI(binaryFile)) { - err(`warning: Loading from a file URI (${binaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`); - } - abort(reason); - } -} - -async function instantiateAsync(binary, binaryFile, imports) { - // Cross-Origin Storage (COS) progressive enhancement. - // https://github.com/WICG/cross-origin-storage - // - // COS is only beneficial when this .wasm binary is byte-identical across - // many origins — i.e. a popular library loaded by many independent sites. - // Application-specific Wasm gains nothing from COS that the normal HTTP - // cache does not already provide. - // - // The SHA-256 hash of the final .wasm binary is computed at link time and - // embedded here as a build-time constant. At runtime we feature-detect the - // browser COS API via `'crossOriginStorage' in navigator`, then call - // navigator.crossOriginStorage.requestFileHandles() with the hash object - // required by the spec ({ algorithm: 'SHA-256', value: '' }). - // - // Cache-hit path: - // requestFileHandles() succeeds → getFile() → arrayBuffer() → instantiate - // - // Cache-miss path (NotFoundError): - // fetch() the wasm over the network → instantiate → store in COS. - // The origins field is controlled by -sCROSS_ORIGIN_STORAGE_ORIGINS - // (default: '*', globally available). The store is fire-and-forget so - // it never delays startup. - // - // Visibility and security: the spec allows widening visibility (e.g. a - // restricted entry promoted to globally available) but never narrowing it. - // Because storing always requires writing the actual bytes, no third party - // can probe the cache to discover whether a restricted entry was previously - // stored by another origin — a cache hit is only possible after an explicit - // write that provided the content. - // - // Any other error (NotAllowedError, network failure, …) falls through to the - // standard Emscripten streaming path so the page always loads. - var cosHash = { algorithm: 'SHA-256', value: 'a617cb51176b7e0c729fb776ff688310ecbe035a0da79fcac66b1bf98028e793' }; - if (cosHash.value && 'crossOriginStorage' in navigator) { - try { - var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); - // Cache hit — read the Blob and instantiate from its ArrayBuffer. - var cosFile = await cosHandles[0].getFile(); - var cosBytes = await cosFile.arrayBuffer(); - // Optional instrumentation callback: Module['onCOSCacheHit'](hash) - // Called when the Wasm binary is served from the cross-origin cache. - if (typeof Module['onCOSCacheHit'] == 'function') Module['onCOSCacheHit'](cosHash.value); - return WebAssembly.instantiate(cosBytes, imports); - } catch (cosErr) { - if (cosErr.name === 'NotFoundError') { - // Cache miss — fetch normally, then store in COS for future consumers. - try { - var networkResponse = await fetch(binaryFile, { credentials: 'same-origin' }); - var wasmBytes = await networkResponse.arrayBuffer(); - // Optional instrumentation callback: Module['onCOSCacheMiss'](hash, url) - // Called when the Wasm binary is not in COS and is fetched over the network. - if (typeof Module['onCOSCacheMiss'] == 'function') Module['onCOSCacheMiss'](cosHash.value, binaryFile); - // Fire-and-forget store; never block instantiation on the write. - (async () => { - try { - var writeHandles = await navigator.crossOriginStorage.requestFileHandles( - [cosHash], - { create: true, origins: '*' }, - ); - var writable = await writeHandles[0].createWritable(); - await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); - await writable.close(); - // Optional instrumentation callback: Module['onCOSStore'](hash) - // Called after the Wasm binary has been successfully written to COS. - if (typeof Module['onCOSStore'] == 'function') Module['onCOSStore'](cosHash.value); - } catch (storeErr) { - err(`COS store failed: ${storeErr}`); - } - })(); - return WebAssembly.instantiate(wasmBytes, imports); - } catch (fetchErr) { - // Network fetch failed; fall through to the standard path. - err(`COS fallback fetch failed: ${fetchErr}`); - } - } else if (cosErr.name === 'NotAllowedError') { - err(`COS: permission denied.`); - } else { - err(`Cross-Origin Storage lookup failed: ${cosErr}`); - } - } - } - if (!binary - ) { - try { - var response = fetch(binaryFile, { credentials: 'same-origin' }); - var instantiationResult = await WebAssembly.instantiateStreaming(response, imports); - return instantiationResult; - } catch (reason) { - // We expect the most common failure cause to be a bad MIME type for the binary, - // in which case falling back to ArrayBuffer instantiation should work. - err(`wasm streaming compile failed: ${reason}`); - err('falling back to ArrayBuffer instantiation'); - // fall back of instantiateArrayBuffer below - }; - } - return instantiateArrayBuffer(binaryFile, imports); -} - -function getWasmImports() { - // prepare imports - var imports = { - 'env': wasmImports, - 'wasi_snapshot_preview1': wasmImports, - }; - return imports; -} - -// Create the wasm instance. -// Receives the wasm imports, returns the exports. -async function createWasm() { - // Load the wasm module and create an instance of using native support in the JS engine. - // handle a generated wasm instance, receiving its exports and - // performing other necessary setup - /** @param {WebAssembly.Module=} module*/ - function receiveInstance(instance, module) { - wasmExports = instance.exports; - - assignWasmExports(wasmExports); - - updateMemoryViews(); - - removeRunDependency('wasm-instantiate'); - return wasmExports; - } - addRunDependency('wasm-instantiate'); - - // Prefer streaming instantiation if available. - // Async compilation can be confusing when an error on the page overwrites Module - // (for example, if the order of elements is wrong, and the one defining Module is - // later), so we save Module and check it later. - var trueModule = Module; - function receiveInstantiationResult(result) { - // 'result' is a ResultObject object which has both the module and instance. - // receiveInstance() will swap in the exports (to Module.asm) so they can be called - assert(Module === trueModule, 'the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?'); - trueModule = null; - // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. - // When the regression is fixed, can restore the above PTHREADS-enabled path. - return receiveInstance(result['instance']); - } - - var info = getWasmImports(); - - // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback - // to manually instantiate the Wasm module themselves. This allows pages to - // run the instantiation parallel to any other async startup actions they are - // performing. - // Also pthreads and wasm workers initialize the wasm instance through this - // path. - if (Module['instantiateWasm']) { - return new Promise((resolve, reject) => { - try { - Module['instantiateWasm'](info, (inst, mod) => { - resolve(receiveInstance(inst, mod)); - }); - } catch(e) { - err(`Module.instantiateWasm callback failed with error: ${e}`); - reject(e); - } - }); - } - - wasmBinaryFile ??= findWasmBinary(); - var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info); - var exports = receiveInstantiationResult(result); - return exports; -} - -// end include: preamble.js - -// Begin JS library code - - - class ExitStatus { - name = 'ExitStatus'; - constructor(status) { - this.message = `Program terminated with exit(${status})`; - this.status = status; - } - } - - /** @type {!Int16Array} */ - var HEAP16; - - /** @type {!Int32Array} */ - var HEAP32; - - /** not-@type {!BigInt64Array} */ - var HEAP64; - - /** @type {!Int8Array} */ - var HEAP8; - - /** @type {!Float32Array} */ - var HEAPF32; - - /** @type {!Float64Array} */ - var HEAPF64; - - /** @type {!Uint16Array} */ - var HEAPU16; - - /** @type {!Uint32Array} */ - var HEAPU32; - - /** not-@type {!BigUint64Array} */ - var HEAPU64; - - /** @type {!Uint8Array} */ - var HEAPU8; - - var callRuntimeCallbacks = (callbacks) => { - while (callbacks.length > 0) { - // Pass the module as the first argument. - callbacks.shift()(Module); - } - }; - var onPostRuns = []; - var addOnPostRun = (cb) => onPostRuns.push(cb); - - var onPreRuns = []; - var addOnPreRun = (cb) => onPreRuns.push(cb); - - var runDependencies = 0; - - - var dependenciesFulfilled = null; - - var runDependencyTracking = { - }; - - var runDependencyWatcher = null; - var removeRunDependency = (id) => { - runDependencies--; - - Module['monitorRunDependencies']?.(runDependencies); - - assert(id, 'removeRunDependency requires an ID'); - assert(runDependencyTracking[id]); - delete runDependencyTracking[id]; - if (runDependencies == 0) { - if (runDependencyWatcher !== null) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - } - if (dependenciesFulfilled) { - var callback = dependenciesFulfilled; - dependenciesFulfilled = null; - callback(); // can add another dependenciesFulfilled - } - } - }; - - - var addRunDependency = (id) => { - runDependencies++; - - Module['monitorRunDependencies']?.(runDependencies); - - assert(id, 'addRunDependency requires an ID') - assert(!runDependencyTracking[id]); - runDependencyTracking[id] = 1; - if (runDependencyWatcher === null && globalThis.setInterval) { - // Check for missing dependencies every few seconds - runDependencyWatcher = setInterval(() => { - if (ABORT) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - return; - } - var shown = false; - for (var dep in runDependencyTracking) { - if (!shown) { - shown = true; - err('still waiting on run dependencies:'); - } - err(`dependency: ${dep}`); - } - if (shown) { - err('(end of list)'); - } - }, 10000); - } - }; - - - - /** - * @param {number} ptr - * @param {string} type - */ - function getValue(ptr, type = 'i8') { - if (type.endsWith('*')) type = '*'; - switch (type) { - case 'i1': return HEAP8[ptr]; - case 'i8': return HEAP8[ptr]; - case 'i16': return HEAP16[((ptr)>>1)]; - case 'i32': return HEAP32[((ptr)>>2)]; - case 'i64': return HEAP64[((ptr)>>3)]; - case 'float': return HEAPF32[((ptr)>>2)]; - case 'double': return HEAPF64[((ptr)>>3)]; - case '*': return HEAPU32[((ptr)>>2)]; - default: abort(`invalid type for getValue: ${type}`); - } - } - - var noExitRuntime = true; - - function ptrToString(ptr) { - assert(typeof ptr === 'number', `ptrToString expects a number, got ${typeof ptr}`); - // Convert to 32-bit unsigned value - ptr >>>= 0; - return '0x' + ptr.toString(16).padStart(8, '0'); - } - - - - /** - * @param {number} ptr - * @param {number} value - * @param {string} type - */ - function setValue(ptr, value, type = 'i8') { - if (type.endsWith('*')) type = '*'; - switch (type) { - case 'i1': HEAP8[ptr] = value; break; - case 'i8': HEAP8[ptr] = value; break; - case 'i16': HEAP16[((ptr)>>1)] = value; break; - case 'i32': HEAP32[((ptr)>>2)] = value; break; - case 'i64': HEAP64[((ptr)>>3)] = BigInt(value); break; - case 'float': HEAPF32[((ptr)>>2)] = value; break; - case 'double': HEAPF64[((ptr)>>3)] = value; break; - case '*': HEAPU32[((ptr)>>2)] = value; break; - default: abort(`invalid type for setValue: ${type}`); - } - } - - var stackRestore = (val) => __emscripten_stack_restore(val); - - var stackSave = () => _emscripten_stack_get_current(); - - var warnOnce = (text) => { - warnOnce.shown ||= {}; - if (!warnOnce.shown[text]) { - warnOnce.shown[text] = 1; - err(text); - } - }; - - - - var getCFunc = (ident) => { - var func = Module['_' + ident]; // closure exported function - assert(func, `Cannot call unknown function ${ident}, make sure it is exported`); - return func; - }; - - var writeArrayToMemory = (array, buffer) => { - assert(array.length >= 0, 'writeArrayToMemory array must have a length (should be an array or typed array)') - HEAP8.set(array, buffer); - }; - - var lengthBytesUTF8 = (str) => { - var len = 0; - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code - // unit, not a Unicode code point of the character! So decode - // UTF16->UTF32->UTF8. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - var c = str.charCodeAt(i); // possibly a lead surrogate - if (c <= 0x7F) { - len++; - } else if (c <= 0x7FF) { - len += 2; - } else if (c >= 0xD800 && c <= 0xDFFF) { - len += 4; ++i; - } else { - len += 3; - } - } - return len; - }; - - var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { - assert(typeof str === 'string', `stringToUTF8Array expects a string (got ${typeof str})`); - // Parameter maxBytesToWrite is not optional. Negative values, 0, null, - // undefined and false each don't write out any bytes. - if (!(maxBytesToWrite > 0)) - return 0; - - var startIdx = outIdx; - var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. - for (var i = 0; i < str.length; ++i) { - // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description - // and https://www.ietf.org/rfc/rfc2279.txt - // and https://tools.ietf.org/html/rfc3629 - var u = str.codePointAt(i); - if (u <= 0x7F) { - if (outIdx >= endIdx) break; - heap[outIdx++] = u; - } else if (u <= 0x7FF) { - if (outIdx + 1 >= endIdx) break; - heap[outIdx++] = 0xC0 | (u >> 6); - heap[outIdx++] = 0x80 | (u & 63); - } else if (u <= 0xFFFF) { - if (outIdx + 2 >= endIdx) break; - heap[outIdx++] = 0xE0 | (u >> 12); - heap[outIdx++] = 0x80 | ((u >> 6) & 63); - heap[outIdx++] = 0x80 | (u & 63); - } else { - if (outIdx + 3 >= endIdx) break; - if (u > 0x10FFFF) warnOnce(`Invalid Unicode code point ${ptrToString(u)} encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).`); - heap[outIdx++] = 0xF0 | (u >> 18); - heap[outIdx++] = 0x80 | ((u >> 12) & 63); - heap[outIdx++] = 0x80 | ((u >> 6) & 63); - heap[outIdx++] = 0x80 | (u & 63); - // Gotcha: if codePoint is over 0xFFFF, it is represented as a surrogate pair in UTF-16. - // We need to manually skip over the second code unit for correct iteration. - i++; - } - } - // Null-terminate the pointer to the buffer. - heap[outIdx] = 0; - return outIdx - startIdx; - }; - var stringToUTF8 = (str, outPtr, maxBytesToWrite) => { - assert(typeof maxBytesToWrite == 'number', 'stringToUTF8 requires a third parameter that specifies the length of the output buffer'); - return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); - }; - - var stackAlloc = (sz) => __emscripten_stack_alloc(sz); - var stringToUTF8OnStack = (str) => { - var size = lengthBytesUTF8(str) + 1; - var ret = stackAlloc(size); - stringToUTF8(str, ret, size); - return ret; - }; - - - - - var UTF8Decoder = globalThis.TextDecoder && new TextDecoder(); - - var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { - var maxIdx = idx + maxBytesToRead; - if (ignoreNul) return maxIdx; - // TextDecoder needs to know the byte length in advance, it doesn't stop on - // null terminator by itself. - // As a tiny code save trick, compare idx against maxIdx using a negation, - // so that maxBytesToRead=undefined/NaN means Infinity. - while (heapOrArray[idx] && !(idx >= maxIdx)) ++idx; - return idx; - }; - - - /** - * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given - * array that contains uint8 values, returns a copy of that string as a - * Javascript String object. - * heapOrArray is either a regular array, or a JavaScript typed array view. - * @param {number=} idx - * @param {number=} maxBytesToRead - * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. - * @return {string} - */ - var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead, ignoreNul) => { - - var endPtr = findStringEnd(heapOrArray, idx, maxBytesToRead, ignoreNul); - - // When using conditional TextDecoder, skip it for short strings as the overhead of the native call is not worth it. - if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) { - return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr)); - } - var str = ''; - while (idx < endPtr) { - // For UTF8 byte structure, see: - // http://en.wikipedia.org/wiki/UTF-8#Description - // https://www.ietf.org/rfc/rfc2279.txt - // https://tools.ietf.org/html/rfc3629 - var u0 = heapOrArray[idx++]; - if (!(u0 & 0x80)) { str += String.fromCharCode(u0); continue; } - var u1 = heapOrArray[idx++] & 63; - if ((u0 & 0xE0) == 0xC0) { str += String.fromCharCode(((u0 & 31) << 6) | u1); continue; } - var u2 = heapOrArray[idx++] & 63; - if ((u0 & 0xF0) == 0xE0) { - u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; - } else { - if ((u0 & 0xF8) != 0xF0) warnOnce(`Invalid UTF-8 leading byte ${ptrToString(u0)} encountered when deserializing a UTF-8 string in wasm memory to a JS string!`); - u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heapOrArray[idx++] & 63); - } - - if (u0 < 0x10000) { - str += String.fromCharCode(u0); - } else { - var ch = u0 - 0x10000; - str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); - } - } - return str; - }; - - /** - * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the - * emscripten HEAP, returns a copy of that string as a Javascript String object. - * - * @param {number} ptr - * @param {number=} maxBytesToRead - An optional length that specifies the - * maximum number of bytes to read. You can omit this parameter to scan the - * string until the first 0 byte. If maxBytesToRead is passed, and the string - * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the - * string will cut short at that byte index. - * @param {boolean=} ignoreNul - If true, the function will not stop on a NUL character. - * @return {string} - */ - var UTF8ToString = (ptr, maxBytesToRead, ignoreNul) => { - assert(typeof ptr == 'number', `UTF8ToString expects a number (got ${typeof ptr})`); - return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead, ignoreNul) : ''; - }; - - /** - * @param {string|null=} returnType - * @param {Array=} argTypes - * @param {Array=} args - * @param {Object=} opts - */ - var ccall = (ident, returnType, argTypes, args, opts) => { - // For fast lookup of conversion functions - var toC = { - 'string': (str) => { - var ret = 0; - if (str !== null && str !== undefined && str !== 0) { // null string - ret = stringToUTF8OnStack(str); - } - return ret; - }, - 'array': (arr) => { - var ret = stackAlloc(arr.length); - writeArrayToMemory(arr, ret); - return ret; - } - }; - - function convertReturnValue(ret) { - if (returnType === 'string') { - return UTF8ToString(ret); - } - if (returnType === 'boolean') return Boolean(ret); - return ret; - } - - var func = getCFunc(ident); - var cArgs = []; - var stack = 0; - assert(returnType !== 'array', 'return type should not be "array"'); - if (args) { - for (var i = 0; i < args.length; i++) { - var converter = toC[argTypes[i]]; - if (converter) { - if (stack === 0) stack = stackSave(); - cArgs[i] = converter(args[i]); - } else { - cArgs[i] = args[i]; - } - } - } - var ret = func(...cArgs); - function onDone(ret) { - if (stack !== 0) stackRestore(stack); - return convertReturnValue(ret); - } - - ret = onDone(ret); - return ret; - }; -// End JS library code - -// include: postlibrary.js -// This file is included after the automatically-generated JS library code -// but before the wasm module is created. - -{ - - // Begin ATMODULES hooks - if (Module['noExitRuntime']) noExitRuntime = Module['noExitRuntime']; -if (Module['print']) out = Module['print']; -if (Module['printErr']) err = Module['printErr']; -if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; - -Module['FS_createDataFile'] = FS.createDataFile; -Module['FS_createPreloadedFile'] = FS.createPreloadedFile; - - // End ATMODULES hooks - - checkIncomingModuleAPI(); - - if (Module['arguments']) programArgs = Module['arguments']; - if (Module['thisProgram']) thisProgram = Module['thisProgram']; - - // Assertions on removed incoming Module JS APIs. - assert(typeof Module['memoryInitializerPrefixURL'] == 'undefined', 'Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead'); - assert(typeof Module['pthreadMainPrefixURL'] == 'undefined', 'Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead'); - assert(typeof Module['cdInitializerPrefixURL'] == 'undefined', 'Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead'); - assert(typeof Module['filePackagePrefixURL'] == 'undefined', 'Module.filePackagePrefixURL option was removed, use Module.locateFile instead'); - assert(typeof Module['read'] == 'undefined', 'Module.read option was removed'); - assert(typeof Module['readAsync'] == 'undefined', 'Module.readAsync option was removed (modify readAsync in JS)'); - assert(typeof Module['readBinary'] == 'undefined', 'Module.readBinary option was removed (modify readBinary in JS)'); - assert(typeof Module['setWindowTitle'] == 'undefined', 'Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)'); - assert(typeof Module['TOTAL_MEMORY'] == 'undefined', 'Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY'); - assert(typeof Module['ENVIRONMENT'] == 'undefined', 'Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)'); - assert(typeof Module['STACK_SIZE'] == 'undefined', 'STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time') - // If memory is defined in wasm, the user can't provide it, or set INITIAL_MEMORY - assert(typeof Module['wasmMemory'] == 'undefined', 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally'); - assert(typeof Module['INITIAL_MEMORY'] == 'undefined', 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically'); - - if (Module['preInit']) { - if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; - while (Module['preInit'].length > 0) { - Module['preInit'].shift()(); - } - } - consumedModuleProp('preInit'); -} - -// Begin runtime exports - Module['ccall'] = ccall; - var missingLibrarySymbols = [ - 'writeI53ToI64', - 'writeI53ToI64Clamped', - 'writeI53ToI64Signaling', - 'writeI53ToU64Clamped', - 'writeI53ToU64Signaling', - 'readI53FromI64', - 'readI53FromU64', - 'convertI32PairToI53', - 'convertI32PairToI53Checked', - 'convertU32PairToI53', - 'bigintToI53Checked', - 'getTempRet0', - 'setTempRet0', - 'createNamedFunction', - 'zeroMemory', - 'exitJS', - 'getHeapMax', - 'growMemory', - 'withStackSave', - 'strError', - 'inetPton4', - 'inetNtop4', - 'inetPton6', - 'inetNtop6', - 'readSockaddr', - 'writeSockaddr', - 'readEmAsmArgs', - 'jstoi_q', - 'getExecutableName', - 'autoResumeAudioContext', - 'getDynCaller', - 'dynCall', - 'handleException', - 'keepRuntimeAlive', - 'runtimeKeepalivePush', - 'runtimeKeepalivePop', - 'callUserCallback', - 'maybeExit', - 'asyncLoad', - 'asmjsMangle', - 'alignMemory', - 'mmapAlloc', - 'HandleAllocator', - 'getUniqueRunDependency', - 'addOnInit', - 'addOnPostCtor', - 'addOnPreMain', - 'addOnExit', - 'STACK_SIZE', - 'STACK_ALIGN', - 'POINTER_SIZE', - 'ASSERTIONS', - 'cwrap', - 'convertJsFunctionToWasm', - 'getEmptyTableSlot', - 'updateTableMap', - 'getFunctionAddress', - 'addFunction', - 'removeFunction', - 'intArrayFromString', - 'intArrayToString', - 'AsciiToString', - 'stringToAscii', - 'UTF16ToString', - 'stringToUTF16', - 'lengthBytesUTF16', - 'UTF32ToString', - 'stringToUTF32', - 'lengthBytesUTF32', - 'stringToNewUTF8', - 'registerKeyEventCallback', - 'maybeCStringToJsString', - 'findEventTarget', - 'getBoundingClientRect', - 'fillMouseEventData', - 'registerMouseEventCallback', - 'registerWheelEventCallback', - 'registerUiEventCallback', - 'registerFocusEventCallback', - 'fillDeviceOrientationEventData', - 'registerDeviceOrientationEventCallback', - 'fillDeviceMotionEventData', - 'registerDeviceMotionEventCallback', - 'screenOrientation', - 'fillOrientationChangeEventData', - 'registerOrientationChangeEventCallback', - 'fillFullscreenChangeEventData', - 'registerFullscreenChangeEventCallback', - 'JSEvents_requestFullscreen', - 'JSEvents_resizeCanvasForFullscreen', - 'registerRestoreOldStyle', - 'hideEverythingExceptGivenElement', - 'restoreHiddenElements', - 'setLetterbox', - 'softFullscreenResizeWebGLRenderTarget', - 'doRequestFullscreen', - 'fillPointerlockChangeEventData', - 'registerPointerlockChangeEventCallback', - 'registerPointerlockErrorEventCallback', - 'requestPointerLock', - 'fillVisibilityChangeEventData', - 'registerVisibilityChangeEventCallback', - 'registerTouchEventCallback', - 'fillGamepadEventData', - 'registerGamepadEventCallback', - 'registerBeforeUnloadEventCallback', - 'fillBatteryEventData', - 'registerBatteryEventCallback', - 'setCanvasElementSize', - 'getCanvasElementSize', - 'jsStackTrace', - 'getCallstack', - 'convertPCtoSourceLocation', - 'getEnvStrings', - 'checkWasiClock', - 'flush_NO_FILESYSTEM', - 'wasiRightsToMuslOFlags', - 'wasiOFlagsToMuslOFlags', - 'initRandomFill', - 'randomFill', - 'safeSetTimeout', - 'setImmediateWrapped', - 'safeRequestAnimationFrame', - 'clearImmediateWrapped', - 'registerPostMainLoop', - 'registerPreMainLoop', - 'getPromise', - 'makePromise', - 'addPromise', - 'idsToPromises', - 'makePromiseCallback', - 'ExceptionInfo', - 'findMatchingCatch', - 'incrementUncaughtExceptionCount', - 'decrementUncaughtExceptionCount', - 'Browser_asyncPrepareDataCounter', - 'isLeapYear', - 'ydayFromDate', - 'arraySum', - 'addDays', - 'getSocketFromFD', - 'getSocketAddress', - 'FS_createPreloadedFile', - 'FS_preloadFile', - 'FS_modeStringToFlags', - 'FS_getMode', - 'FS_fileDataToTypedArray', - 'FS_stdin_getChar', - 'FS_mkdirTree', - '_setNetworkCallback', - 'heapObjectForWebGLType', - 'toTypedArrayIndex', - 'webgl_enable_ANGLE_instanced_arrays', - 'webgl_enable_OES_vertex_array_object', - 'webgl_enable_WEBGL_draw_buffers', - 'webgl_enable_WEBGL_multi_draw', - 'webgl_enable_EXT_polygon_offset_clamp', - 'webgl_enable_EXT_clip_control', - 'webgl_enable_WEBGL_polygon_mode', - 'emscriptenWebGLGet', - 'computeUnpackAlignedImageSize', - 'colorChannelsInGlTextureFormat', - 'emscriptenWebGLGetTexPixelData', - 'emscriptenWebGLGetUniform', - 'webglGetProgramUniformLocation', - 'webglGetUniformLocation', - 'webglPrepareUniformLocationsBeforeFirstUse', - 'webglGetLeftBracePos', - 'emscriptenWebGLGetVertexAttrib', - '__glGetActiveAttribOrUniform', - 'writeGLArray', - 'registerWebGlEventCallback', - 'runAndAbortIfError', - 'ALLOC_NORMAL', - 'ALLOC_STACK', - 'allocate', - 'writeStringToMemory', - 'writeAsciiToMemory', - 'allocateUTF8', - 'allocateUTF8OnStack', - 'demangle', - 'stackTrace', - 'getNativeTypeSize', -]; -missingLibrarySymbols.forEach(missingLibrarySymbol) - - var unexportedSymbols = [ - 'run', - 'out', - 'err', - 'callMain', - 'abort', - 'wasmExports', - 'writeStackCookie', - 'checkStackCookie', - 'INT53_MAX', - 'INT53_MIN', - 'HEAP8', - 'HEAPU8', - 'HEAP16', - 'HEAPU16', - 'HEAP32', - 'HEAPU32', - 'HEAPF32', - 'HEAPF64', - 'HEAP64', - 'HEAPU64', - 'stackSave', - 'stackRestore', - 'stackAlloc', - 'ptrToString', - 'ENV', - 'ERRNO_CODES', - 'DNS', - 'Protocols', - 'Sockets', - 'timers', - 'warnOnce', - 'readEmAsmArgsArray', - 'wasmTable', - 'wasmMemory', - 'noExitRuntime', - 'addRunDependency', - 'removeRunDependency', - 'addOnPreRun', - 'addOnPostRun', - 'freeTableIndexes', - 'functionsInTableMap', - 'setValue', - 'getValue', - 'PATH', - 'PATH_FS', - 'UTF8Decoder', - 'UTF8ArrayToString', - 'UTF8ToString', - 'stringToUTF8Array', - 'stringToUTF8', - 'lengthBytesUTF8', - 'UTF16Decoder', - 'stringToUTF8OnStack', - 'writeArrayToMemory', - 'JSEvents', - 'specialHTMLTargets', - 'findCanvasEventTarget', - 'currentFullscreenStrategy', - 'restoreOldWindowedStyle', - 'UNWIND_CACHE', - 'ExitStatus', - 'emSetImmediate', - 'emClearImmediate_deps', - 'emClearImmediate', - 'promiseMap', - 'uncaughtExceptionCount', - 'exceptionCaught', - 'Browser', - 'requestFullscreen', - 'requestFullScreen', - 'setCanvasSize', - 'getUserMedia', - 'createContext', - 'getPreloadedImageData__data', - 'wget', - 'MONTH_DAYS_REGULAR', - 'MONTH_DAYS_LEAP', - 'MONTH_DAYS_REGULAR_CUMULATIVE', - 'MONTH_DAYS_LEAP_CUMULATIVE', - 'SYSCALLS', - 'preloadPlugins', - 'FS_stdin_getChar_buffer', - 'FS_unlink', - 'FS_createPath', - 'FS_createDevice', - 'FS_readFile', - 'FS', - 'FS_root', - 'FS_mounts', - 'FS_devices', - 'FS_streams', - 'FS_nextInode', - 'FS_nameTable', - 'FS_currentPath', - 'FS_initialized', - 'FS_ignorePermissions', - 'FS_filesystems', - 'FS_syncFSRequests', - 'FS_lookupPath', - 'FS_getPath', - 'FS_hashName', - 'FS_hashAddNode', - 'FS_hashRemoveNode', - 'FS_lookupNode', - 'FS_createNode', - 'FS_destroyNode', - 'FS_isRoot', - 'FS_isMountpoint', - 'FS_isFile', - 'FS_isDir', - 'FS_isLink', - 'FS_isChrdev', - 'FS_isBlkdev', - 'FS_isFIFO', - 'FS_isSocket', - 'FS_flagsToPermissionString', - 'FS_nodePermissions', - 'FS_mayLookup', - 'FS_mayCreate', - 'FS_mayDelete', - 'FS_mayOpen', - 'FS_checkOpExists', - 'FS_nextfd', - 'FS_getStreamChecked', - 'FS_getStream', - 'FS_createStream', - 'FS_closeStream', - 'FS_dupStream', - 'FS_doSetAttr', - 'FS_chrdev_stream_ops', - 'FS_major', - 'FS_minor', - 'FS_makedev', - 'FS_registerDevice', - 'FS_getDevice', - 'FS_getMounts', - 'FS_syncfs', - 'FS_mount', - 'FS_unmount', - 'FS_lookup', - 'FS_mknod', - 'FS_statfs', - 'FS_statfsStream', - 'FS_statfsNode', - 'FS_create', - 'FS_mkdir', - 'FS_mkdev', - 'FS_symlink', - 'FS_rename', - 'FS_rmdir', - 'FS_readdir', - 'FS_readlink', - 'FS_stat', - 'FS_fstat', - 'FS_lstat', - 'FS_doChmod', - 'FS_chmod', - 'FS_lchmod', - 'FS_fchmod', - 'FS_doChown', - 'FS_chown', - 'FS_lchown', - 'FS_fchown', - 'FS_doTruncate', - 'FS_truncate', - 'FS_ftruncate', - 'FS_utime', - 'FS_open', - 'FS_close', - 'FS_isClosed', - 'FS_llseek', - 'FS_read', - 'FS_write', - 'FS_mmap', - 'FS_msync', - 'FS_ioctl', - 'FS_writeFile', - 'FS_cwd', - 'FS_chdir', - 'FS_createDefaultDirectories', - 'FS_createDefaultDevices', - 'FS_createSpecialDirectories', - 'FS_createStandardStreams', - 'FS_staticInit', - 'FS_init', - 'FS_quit', - 'FS_findObject', - 'FS_analyzePath', - 'FS_createFile', - 'FS_createDataFile', - 'FS_forceLoadFile', - 'FS_createLazyFile', - 'MEMFS', - 'TTY', - 'PIPEFS', - 'SOCKFS', - 'tempFixedLengthArray', - 'miniTempWebGLFloatBuffers', - 'miniTempWebGLIntBuffers', - 'GL', - 'AL', - 'GLUT', - 'EGL', - 'GLEW', - 'IDBStore', - 'SDL', - 'SDL_gfx', - 'print', - 'printErr', - 'jstoi_s', -]; -unexportedSymbols.forEach(unexportedRuntimeSymbol); - - // End runtime exports - // Begin JS library exports - // End JS library exports - -// end include: postlibrary.js - -function checkIncomingModuleAPI() { - ignoredModuleProp('fetchSettings'); - ignoredModuleProp('logReadFiles'); - ignoredModuleProp('loadSplitModule'); - ignoredModuleProp('onMalloc'); - ignoredModuleProp('onRealloc'); - ignoredModuleProp('onFree'); - ignoredModuleProp('onSbrkGrow'); -} - -// Imports from the Wasm binary. -var _greet = Module['_greet'] = makeInvalidEarlyAccess('_greet'); -var _fflush = makeInvalidEarlyAccess('_fflush'); -var _emscripten_stack_init = makeInvalidEarlyAccess('_emscripten_stack_init'); -var _emscripten_stack_get_free = makeInvalidEarlyAccess('_emscripten_stack_get_free'); -var _emscripten_stack_get_base = makeInvalidEarlyAccess('_emscripten_stack_get_base'); -var _emscripten_stack_get_end = makeInvalidEarlyAccess('_emscripten_stack_get_end'); -var __emscripten_stack_restore = makeInvalidEarlyAccess('__emscripten_stack_restore'); -var __emscripten_stack_alloc = makeInvalidEarlyAccess('__emscripten_stack_alloc'); -var _emscripten_stack_get_current = makeInvalidEarlyAccess('_emscripten_stack_get_current'); -var memory = makeInvalidEarlyAccess('memory'); -var __indirect_function_table = makeInvalidEarlyAccess('__indirect_function_table'); -var wasmMemory = makeInvalidEarlyAccess('wasmMemory'); - -function assignWasmExports(wasmExports) { - assert(typeof wasmExports['greet'] != 'undefined', 'missing Wasm export: greet'); - assert(typeof wasmExports['fflush'] != 'undefined', 'missing Wasm export: fflush'); - assert(typeof wasmExports['emscripten_stack_init'] != 'undefined', 'missing Wasm export: emscripten_stack_init'); - assert(typeof wasmExports['emscripten_stack_get_free'] != 'undefined', 'missing Wasm export: emscripten_stack_get_free'); - assert(typeof wasmExports['emscripten_stack_get_base'] != 'undefined', 'missing Wasm export: emscripten_stack_get_base'); - assert(typeof wasmExports['emscripten_stack_get_end'] != 'undefined', 'missing Wasm export: emscripten_stack_get_end'); - assert(typeof wasmExports['_emscripten_stack_restore'] != 'undefined', 'missing Wasm export: _emscripten_stack_restore'); - assert(typeof wasmExports['_emscripten_stack_alloc'] != 'undefined', 'missing Wasm export: _emscripten_stack_alloc'); - assert(typeof wasmExports['emscripten_stack_get_current'] != 'undefined', 'missing Wasm export: emscripten_stack_get_current'); - assert(typeof wasmExports['memory'] != 'undefined', 'missing Wasm export: memory'); - assert(typeof wasmExports['__indirect_function_table'] != 'undefined', 'missing Wasm export: __indirect_function_table'); - _greet = Module['_greet'] = createExportWrapper('greet', 0); - _fflush = createExportWrapper('fflush', 1); - _emscripten_stack_init = wasmExports['emscripten_stack_init']; - _emscripten_stack_get_free = wasmExports['emscripten_stack_get_free']; - _emscripten_stack_get_base = wasmExports['emscripten_stack_get_base']; - _emscripten_stack_get_end = wasmExports['emscripten_stack_get_end']; - __emscripten_stack_restore = wasmExports['_emscripten_stack_restore']; - __emscripten_stack_alloc = wasmExports['_emscripten_stack_alloc']; - _emscripten_stack_get_current = wasmExports['emscripten_stack_get_current']; - memory = wasmMemory = wasmExports['memory']; - __indirect_function_table = wasmExports['__indirect_function_table']; -} - -var wasmImports = { - -}; - - -// include: postamble.js -// === Auto-generated postamble setup entry stuff === - -var calledRun; - -function stackCheckInit() { - // This is normally called automatically during __wasm_call_ctors but need to - // get these values before even running any of the ctors so we call it redundantly - // here. - _emscripten_stack_init(); - // TODO(sbc): Move writeStackCookie to native to to avoid this. - writeStackCookie(); -} - -function run() { - - if (runDependencies > 0) { - dependenciesFulfilled = run; - return; - } - - stackCheckInit(); - - preRun(); - - // a preRun added a dependency, run will be called later - if (runDependencies > 0) { - dependenciesFulfilled = run; - return; - } - - function doRun() { - // run may have just been called through dependencies being fulfilled just in this very frame, - // or while the async setStatus time below was happening - assert(!calledRun); - calledRun = true; - Module['calledRun'] = true; - - if (ABORT) return; - - initRuntime(); - - Module['onRuntimeInitialized']?.(); - consumedModuleProp('onRuntimeInitialized'); - - assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'); - - postRun(); - } - - if (Module['setStatus']) { - Module['setStatus']('Running...'); - setTimeout(() => { - setTimeout(() => Module['setStatus'](''), 1); - doRun(); - }, 1); - } else - { - doRun(); - } - checkStackCookie(); -} - -function checkUnflushedContent() { - // Compiler settings do not allow exiting the runtime, so flushing - // the streams is not possible. but in ASSERTIONS mode we check - // if there was something to flush, and if so tell the user they - // should request that the runtime be exitable. - // Normally we would not even include flush() at all, but in ASSERTIONS - // builds we do so just for this check, and here we see if there is any - // content to flush, that is, we check if there would have been - // something a non-ASSERTIONS build would have not seen. - // How we flush the streams depends on whether we are in SYSCALLS_REQUIRE_FILESYSTEM=0 - // mode (which has its own special function for this; otherwise, all - // the code is inside libc) - var oldOut = out; - var oldErr = err; - var has = false; - out = err = (x) => { - has = true; - } - try { // it doesn't matter if it fails - _fflush(0); - } catch(e) {} - out = oldOut; - err = oldErr; - if (has) { - warnOnce('stdio streams had content in them that was not flushed. you should set EXIT_RUNTIME to 1 (see the Emscripten FAQ), or make sure to emit a newline when you printf etc.'); - warnOnce('(this may also be due to not including full filesystem support - try building with -sFORCE_FILESYSTEM)'); - } -} - -var wasmExports; - -// With async instantation wasmExports is assigned asynchronously when the -// instance is received. -createWasm(); - -run(); - -// end include: postamble.js - diff --git a/test/cross_origin_storage/index.wasm b/test/cross_origin_storage/index.wasm deleted file mode 100755 index a7b17d387c1852563c9f194925da15d7892a7698..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 994 zcmaJ=&2H2%5T3EK+hyH0TPV<0m2kIvSSiXCI5tQi-hgA8O}$Il{MB&^y)>ah{2w`R z;J^d$1UwzK(@Kb~kP>_R%{SjnCS!=I3;_Ti^p9!NG}!3vqQQ$}fMd`nY(T%*PkmtE9+770ptC(4WuKx_Sx2yCSP%nUqT8VWpyYsbdla zWOoH$2rZ+Pf!^$!MU?<&=f*gZpFnRnT=7X!>6!(w`>Q_vMMVtj`zt}Fkhqpo|D@mQy4Z1l;*addT>eH-cOl%sCoGgC@7MpI_&UAsM#qf~qb9e%e=*Vuw(E~cvNhX%V_HV`9+-j0W_7ysyDQ+i ze;xdyRNg&v7yRqV_lq+j(;{DZ!>mqKau%hv@Z7An7r|eo#VkrI?@nIiN=9Xw Date: Tue, 9 Jun 2026 18:30:55 +0200 Subject: [PATCH 48/87] test: log COS events to console in miss_then_hit browser test Makes the cache-miss store and cache-hit paths visible in the Chrome console output, which aids debugging and confirms the extension keyed the entry on the correct hash. --- test/test_browser.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/test_browser.py b/test/test_browser.py index f38e57db9f0f5..3ee41b97c0a1e 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5745,8 +5745,14 @@ def test_cross_origin_storage_miss_then_hit(self): # COS callbacks; otherwise Emscripten aborts on the unknown Module props. create_file('cos_pre.js', ''' var Module = { - onCOSStore: function(hash) { reportResultToServer('stored'); }, - onCOSCacheHit: function(hash) { reportResultToServer('cache-hit'); }, + onCOSStore: function(hash) { + console.log('[COS] stored, SHA-256:', hash); + reportResultToServer('stored'); + }, + onCOSCacheHit: function(hash) { + console.log('[COS] cache-hit, SHA-256:', hash); + reportResultToServer('cache-hit'); + }, }; ''') self.compile_btest('browser_test_hello_world.c', [ From e980be0e1317c213ab6dd40e4fd5a4493343cd1f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:11:04 +0200 Subject: [PATCH 49/87] style: move [experimental] tag to end of CROSS_ORIGIN_STORAGE comment Per reviewer feedback, [experimental] should be placed at the end of the comment block, adjacent to [link], matching the convention used by all other experimental settings (WASMFS, PURE_WASI, SPLIT_MODULE, etc.). --- src/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings.js b/src/settings.js index d329d4c4d8bcb..797113849527a 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2205,7 +2205,6 @@ var GROWABLE_ARRAYBUFFERS = false; // indirectly using `importScripts` var CROSS_ORIGIN = false; -// [experimental] // Enables Cross-Origin Storage (COS) API support for Wasm // loading on the Web target. At link time Emscripten computes the SHA-256 // hash of the final ``.wasm`` binary and embeds it in the generated JS. @@ -2220,6 +2219,7 @@ var CROSS_ORIGIN = false; // See :ref:`CrossOriginStorage` for the full guide. // // [link] +// [experimental] var CROSS_ORIGIN_STORAGE = 0; // Controls which origins may read the Wasm binary from the COS cache. Only From a4f75c3bcd240740bb2b76b031fbfabc375f4b16 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:18:58 +0200 Subject: [PATCH 50/87] fix: remove redundant cosHash?.value guard in COS preamble wasmHash is unconditionally set inside #if CROSS_ORIGIN_STORAGE so the cosHash?.value truthiness check was always true at this point. Per reviewer feedback, simplify to only check for the API presence. --- src/preamble.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/preamble.js b/src/preamble.js index 0a86cd4f0034e..249dc98946456 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -633,7 +633,7 @@ async function instantiateAsync(binary, binaryFile, imports) { // Any error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. var cosHash = Module['wasmHash']; - if (cosHash?.value && 'crossOriginStorage' in navigator) { + if ('crossOriginStorage' in navigator) { try { var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); // Cache hit — read the Blob and instantiate from its ArrayBuffer. From 8cca63b45e3e6908acf349b76425549ccb16e134 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:28:14 +0200 Subject: [PATCH 51/87] fix: make CROSS_ORIGIN_STORAGE a hard error for non-web environments Previously a warning was emitted when CROSS_ORIGIN_STORAGE was set without a web environment; the preamble.js guard relied on ENVIRONMENT_MAY_BE_WEB to strip the dead COS code. Promote to a hard link-time error so the guard is unnecessary, then drop the outer #if ENVIRONMENT_MAY_BE_WEB wrapper. Per reviewer feedback (r3383088794). --- .../tools_reference/settings_reference.rst | 5 +++-- src/preamble.js | 2 -- src/settings.js | 5 +++-- test/test_other.py | 21 +++++++------------ tools/link.py | 2 +- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index cf6f68af7fcec..dbd3537c5a6ed 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3373,14 +3373,15 @@ fetched from the shared cross-origin cache on a hit, or stored there after a network fetch on a miss; when the API is absent or errors the runtime falls through to the standard fetch path. -Only meaningful for the Web environment. Incompatible with SINGLE_FILE and +Requires the Web environment; using it without ``-sENVIRONMENT=web`` is a +hard link-time error. Incompatible with SINGLE_FILE and WASM_ASYNC_COMPILATION=0 (both produce hard link-time errors). See :ref:`CrossOriginStorage` for the full guide. .. note:: This is an experimental setting -Default value: 0 +Default value: false .. _cross_origin_storage_origins: diff --git a/src/preamble.js b/src/preamble.js index 249dc98946456..201da8ab4e0f2 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -626,7 +626,6 @@ async function instantiateArrayBuffer(binaryFile, imports) { async function instantiateAsync(binary, binaryFile, imports) { #if !SINGLE_FILE -#if ENVIRONMENT_MAY_BE_WEB #if CROSS_ORIGIN_STORAGE // Cross-Origin Storage (COS) progressive enhancement. // https://github.com/WICG/cross-origin-storage @@ -689,7 +688,6 @@ async function instantiateAsync(binary, binaryFile, imports) { } } #endif // CROSS_ORIGIN_STORAGE -#endif // ENVIRONMENT_MAY_BE_WEB if (!binary #if MIN_SAFARI_VERSION < 150000 // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming diff --git a/src/settings.js b/src/settings.js index 797113849527a..5d5b91d05982a 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2213,14 +2213,15 @@ var CROSS_ORIGIN = false; // a network fetch on a miss; when the API is absent or errors the runtime // falls through to the standard fetch path. // -// Only meaningful for the Web environment. Incompatible with SINGLE_FILE and +// Requires the Web environment; using it without ``-sENVIRONMENT=web`` is a +// hard link-time error. Incompatible with SINGLE_FILE and // WASM_ASYNC_COMPILATION=0 (both produce hard link-time errors). // // See :ref:`CrossOriginStorage` for the full guide. // // [link] // [experimental] -var CROSS_ORIGIN_STORAGE = 0; +var CROSS_ORIGIN_STORAGE = false; // Controls which origins may read the Wasm binary from the COS cache. Only // meaningful when ``-sCROSS_ORIGIN_STORAGE`` is set. Applied only during the diff --git a/test/test_other.py b/test/test_other.py index 04d85614e3ad1..af080c0acf342 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15649,20 +15649,13 @@ def test_cross_origin_storage_disabled_by_default(self): js = read_file('hello.js') self.assertNotContained('crossOriginStorage', js) - def test_cross_origin_storage_not_emitted_for_node_target(self): - """COS code must NOT appear when targeting Node.js only, even with the flag set. - - The #if ENVIRONMENT_MAY_BE_WEB guard strips it. A warning must also be - emitted since the flag does nothing in this configuration. - """ - proc = self.run_process([EMCC, test_file('hello_world.cpp'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=node', - '-o', 'hello.js'], - stderr=PIPE) - self.assertNotContained('crossOriginStorage', read_file('hello.js')) - self.assertContained('CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web', - proc.stderr) + def test_cross_origin_storage_error_for_non_web_target(self): + """CROSS_ORIGIN_STORAGE + non-web environment must be a hard link-time error.""" + self.assert_fail([EMCC, test_file('hello_world.cpp'), + '-sCROSS_ORIGIN_STORAGE', + '-sENVIRONMENT=node', + '-o', 'hello.js'], + 'CROSS_ORIGIN_STORAGE requires a web environment') def test_cross_origin_storage_error_with_single_file(self): """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" diff --git a/tools/link.py b/tools/link.py index eb2138d7246be..f0ac0dd9ef1cc 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1211,7 +1211,7 @@ def limit_incoming_module_api(): if settings.CROSS_ORIGIN_STORAGE: if not settings.ENVIRONMENT_MAY_BE_WEB: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect when the target environment does not include the web (navigator.crossOriginStorage is not available outside the browser)') + exit_with_error('CROSS_ORIGIN_STORAGE requires a web environment (navigator.crossOriginStorage is not available outside the browser)') if settings.SPLIT_MODULE: diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') if settings.MAIN_MODULE: From 206485ee6903c6b3d275a8779761297d48c5590b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:32:07 +0200 Subject: [PATCH 52/87] remove: Makefile from COS example directory The README already contains the full build command; the Makefile is redundant and raises cross-platform concerns since make.exe isn't guaranteed. Per reviewer feedback (r3383095225). --- test/cross_origin_storage/Makefile | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 test/cross_origin_storage/Makefile diff --git a/test/cross_origin_storage/Makefile b/test/cross_origin_storage/Makefile deleted file mode 100644 index 024872280c5e6..0000000000000 --- a/test/cross_origin_storage/Makefile +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2026 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. - -EMCC ?= emcc - -.PHONY: all clean - -all: index.js - -index.js: main.cpp - $(EMCC) main.cpp -o index.js \ - -O2 \ - -sCROSS_ORIGIN_STORAGE \ - -sENVIRONMENT=web \ - -sEXPORTED_RUNTIME_METHODS=ccall \ - -sEXPORTED_FUNCTIONS=_greet \ - -sALLOW_MEMORY_GROWTH - -clean: - rm -f index.js index.wasm From e872e7b9386fb8070895425b7897cc6cd5254412 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:35:23 +0200 Subject: [PATCH 53/87] style: drop -o flag from COS error-only tests Tests that only check link-time error messages never produce output; the -o flag is unnecessary. Per reviewer feedback (r3383111605). --- test/test_other.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index af080c0acf342..738086951a2d5 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15653,8 +15653,7 @@ def test_cross_origin_storage_error_for_non_web_target(self): """CROSS_ORIGIN_STORAGE + non-web environment must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=node', - '-o', 'hello.js'], + '-sENVIRONMENT=node'], 'CROSS_ORIGIN_STORAGE requires a web environment') def test_cross_origin_storage_error_with_single_file(self): @@ -15662,8 +15661,7 @@ def test_cross_origin_storage_error_with_single_file(self): self.assert_fail([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sSINGLE_FILE', - '-o', 'hello.js'], + '-sSINGLE_FILE'], 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') def test_cross_origin_storage_error_without_async_compilation(self): @@ -15671,8 +15669,7 @@ def test_cross_origin_storage_error_without_async_compilation(self): self.assert_fail([EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sWASM_ASYNC_COMPILATION=0', - '-o', 'hello.js'], + '-sWASM_ASYNC_COMPILATION=0'], 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') def test_cross_origin_storage_warning_with_split_module(self): @@ -15783,8 +15780,7 @@ def test_cross_origin_storage_origins_error_mixed_wildcard(self): [EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]', - '-o', 'hello.js'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]'], "'*' must not be mixed with explicit origins") def test_cross_origin_storage_origins_error_invalid_origin(self): @@ -15793,8 +15789,7 @@ def test_cross_origin_storage_origins_error_invalid_origin(self): [EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]', - '-o', 'hello.js'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]'], 'is not a valid HTTPS origin') def test_cross_origin_storage_origins_error_origin_with_path(self): @@ -15803,8 +15798,7 @@ def test_cross_origin_storage_origins_error_origin_with_path(self): [EMCC, test_file('hello_world.cpp'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]', - '-o', 'hello.js'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]'], 'is not a valid HTTPS origin') def test_cross_origin_storage_wasm_hash_module_property(self): From 84cb3146ff3b2f36a2df39b1c5a1ee871a90d4b8 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:37:28 +0200 Subject: [PATCH 54/87] style: prefer hello_world.c over hello_world.cpp in COS tests Per reviewer feedback (r3383113554). --- test/test_other.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 738086951a2d5..f9b7479cfc8a6 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15588,7 +15588,7 @@ def test_cross_origin_storage_js_output(self): The embedded hash must be the correct SHA-256 of the compiled .wasm file. """ - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) @@ -15631,7 +15631,7 @@ def test_cross_origin_storage_js_output(self): def test_cross_origin_storage_callbacks_opt_in(self): """COS instrumentation callbacks are emitted only when opted in via INCOMING_MODULE_JS_API.""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sINCOMING_MODULE_JS_API=onCOSCacheHit,onCOSCacheMiss,onCOSStore', @@ -15643,7 +15643,7 @@ def test_cross_origin_storage_callbacks_opt_in(self): def test_cross_origin_storage_disabled_by_default(self): """COS code must NOT appear when the flag is omitted (default off).""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') @@ -15651,14 +15651,14 @@ def test_cross_origin_storage_disabled_by_default(self): def test_cross_origin_storage_error_for_non_web_target(self): """CROSS_ORIGIN_STORAGE + non-web environment must be a hard link-time error.""" - self.assert_fail([EMCC, test_file('hello_world.cpp'), + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=node'], 'CROSS_ORIGIN_STORAGE requires a web environment') def test_cross_origin_storage_error_with_single_file(self): """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" - self.assert_fail([EMCC, test_file('hello_world.cpp'), + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sSINGLE_FILE'], @@ -15666,7 +15666,7 @@ def test_cross_origin_storage_error_with_single_file(self): def test_cross_origin_storage_error_without_async_compilation(self): """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must be a hard link-time error.""" - self.assert_fail([EMCC, test_file('hello_world.cpp'), + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sWASM_ASYNC_COMPILATION=0'], @@ -15674,7 +15674,7 @@ def test_cross_origin_storage_error_without_async_compilation(self): def test_cross_origin_storage_warning_with_split_module(self): """CROSS_ORIGIN_STORAGE + SPLIT_MODULE must warn that secondary files are not covered.""" - proc = self.run_process([EMCC, test_file('hello_world.cpp'), + proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sSPLIT_MODULE', @@ -15685,7 +15685,7 @@ def test_cross_origin_storage_warning_with_split_module(self): def test_cross_origin_storage_warning_with_main_module(self): """CROSS_ORIGIN_STORAGE + MAIN_MODULE must warn that side modules are not covered.""" - proc = self.run_process([EMCC, test_file('hello_world.cpp'), + proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sMAIN_MODULE', @@ -15696,7 +15696,7 @@ def test_cross_origin_storage_warning_with_main_module(self): def test_cross_origin_storage_warning_with_side_module(self): """CROSS_ORIGIN_STORAGE + SIDE_MODULE must warn: no JS glue is emitted.""" - proc = self.run_process([EMCC, test_file('hello_world.cpp'), + proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sSIDE_MODULE', '-o', 'hello.wasm'], @@ -15706,7 +15706,7 @@ def test_cross_origin_storage_warning_with_side_module(self): def test_cross_origin_storage_hash_changes_with_content(self): """Two different programs must produce different embedded hashes.""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) @@ -15732,7 +15732,7 @@ def test_cross_origin_storage_origins_default_is_global(self): Globally available; the user only needs -sCROSS_ORIGIN_STORAGE. """ - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) @@ -15743,7 +15743,7 @@ def test_cross_origin_storage_origins_explicit_wildcard(self): This matches the implicit default. """ - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', "-sCROSS_ORIGIN_STORAGE_ORIGINS=['*']", @@ -15752,7 +15752,7 @@ def test_cross_origin_storage_origins_explicit_wildcard(self): def test_cross_origin_storage_origins_explicit_list(self): """An explicit origins list must be emitted as a JS array.""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com', @@ -15764,7 +15764,7 @@ def test_cross_origin_storage_origins_explicit_list(self): def test_cross_origin_storage_origins_same_site(self): """Empty origins list must omit the origins key entirely (same-site only).""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=[]', @@ -15777,7 +15777,7 @@ def test_cross_origin_storage_origins_same_site(self): def test_cross_origin_storage_origins_error_mixed_wildcard(self): """Mixing '*' with explicit origins must be a link-time error.""" self.assert_fail( - [EMCC, test_file('hello_world.cpp'), + [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]'], @@ -15786,7 +15786,7 @@ def test_cross_origin_storage_origins_error_mixed_wildcard(self): def test_cross_origin_storage_origins_error_invalid_origin(self): """A non-HTTPS or malformed origin must be a link-time error.""" self.assert_fail( - [EMCC, test_file('hello_world.cpp'), + [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]'], @@ -15795,7 +15795,7 @@ def test_cross_origin_storage_origins_error_invalid_origin(self): def test_cross_origin_storage_origins_error_origin_with_path(self): """An origin with a path component must be a link-time error.""" self.assert_fail( - [EMCC, test_file('hello_world.cpp'), + [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]'], @@ -15807,7 +15807,7 @@ def test_cross_origin_storage_wasm_hash_module_property(self): Custom Module['instantiateWasm'] implementations bypass instantiateAsync() and can read Module['wasmHash'] to get the build-time hash object. """ - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) @@ -15825,7 +15825,7 @@ def test_cross_origin_storage_wasm_hash_module_property(self): def test_cross_origin_storage_wasm_hash_absent_without_flag(self): """Module['wasmHash'] must NOT appear when CROSS_ORIGIN_STORAGE is off.""" - self.run_process([EMCC, test_file('hello_world.cpp'), + self.run_process([EMCC, test_file('hello_world.c'), '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') From c8e12b214d9c55ec7aee2d226c62843f5d119790 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:41:10 +0200 Subject: [PATCH 55/87] style: drop docstrings and comments from COS tests Test names already convey intent; per reviewer feedback (r3383119508). --- test/test_other.py | 64 ---------------------------------------------- 1 file changed, 64 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index f9b7479cfc8a6..2c542a9df9dec 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15578,59 +15578,31 @@ def test_deprecated_settings(self): self.assertContained('emcc: warning: USE_PTHREADS is deprecated (prefer the standard -pthread flag). Please open a bug if you have a continuing need for this setting [-Wdeprecated]', err) - # --------------------------------------------------------------------------- - # Tests for CROSS_ORIGIN_STORAGE (-sCROSS_ORIGIN_STORAGE) - # https://github.com/WICG/cross-origin-storage - # --------------------------------------------------------------------------- - def test_cross_origin_storage_js_output(self): - """COS code is present in JS when the feature is enabled for the web target. - - The embedded hash must be the correct SHA-256 of the compiled .wasm file. - """ self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') - - # Feature-detect pattern from the WICG explainer. self.assertContained("'crossOriginStorage' in navigator", js) - - # Correct API call. self.assertContained('navigator.crossOriginStorage.requestFileHandles', js) - - # Hash object shape required by the spec. self.assertContained("algorithm: 'SHA-256'", js) - - # Globally-available flag appropriate for a public Wasm module. self.assertContained("origins: '*'", js) - - # Cache-hit and cache-miss path markers. self.assertContained('getFile()', js) self.assertContained('createWritable()', js) - - # Error name discrimination. self.assertContained("'NotFoundError'", js) self.assertContained("'NotAllowedError'", js) - - # Callbacks are opt-in; must be absent from a default build. self.assertNotContained("Module['onCOSCacheHit']", js) self.assertNotContained("Module['onCOSCacheMiss']", js) self.assertNotContained("Module['onCOSStore']", js) - - # The hash embedded in the JS must be a 64-char lowercase hex string … m = re.search(r"value:\s*'([0-9a-f]{64})'", js) self.assertTrue(m, 'could not find a 64-char hex hash value in JS output') embedded_hash = m.group(1) - - # … and must exactly match the SHA-256 of the emitted .wasm file. expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() self.assertEqual(embedded_hash, expected_hash, 'embedded wasm hash does not match actual .wasm SHA-256') def test_cross_origin_storage_callbacks_opt_in(self): - """COS instrumentation callbacks are emitted only when opted in via INCOMING_MODULE_JS_API.""" self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15642,7 +15614,6 @@ def test_cross_origin_storage_callbacks_opt_in(self): self.assertContained("Module['onCOSStore']", js) def test_cross_origin_storage_disabled_by_default(self): - """COS code must NOT appear when the flag is omitted (default off).""" self.run_process([EMCC, test_file('hello_world.c'), '-sENVIRONMENT=web', '-o', 'hello.js']) @@ -15650,14 +15621,12 @@ def test_cross_origin_storage_disabled_by_default(self): self.assertNotContained('crossOriginStorage', js) def test_cross_origin_storage_error_for_non_web_target(self): - """CROSS_ORIGIN_STORAGE + non-web environment must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=node'], 'CROSS_ORIGIN_STORAGE requires a web environment') def test_cross_origin_storage_error_with_single_file(self): - """CROSS_ORIGIN_STORAGE + SINGLE_FILE must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15665,7 +15634,6 @@ def test_cross_origin_storage_error_with_single_file(self): 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') def test_cross_origin_storage_error_without_async_compilation(self): - """CROSS_ORIGIN_STORAGE + WASM_ASYNC_COMPILATION=0 must be a hard link-time error.""" self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15673,7 +15641,6 @@ def test_cross_origin_storage_error_without_async_compilation(self): 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') def test_cross_origin_storage_warning_with_split_module(self): - """CROSS_ORIGIN_STORAGE + SPLIT_MODULE must warn that secondary files are not covered.""" proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15684,7 +15651,6 @@ def test_cross_origin_storage_warning_with_split_module(self): proc.stderr) def test_cross_origin_storage_warning_with_main_module(self): - """CROSS_ORIGIN_STORAGE + MAIN_MODULE must warn that side modules are not covered.""" proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15695,7 +15661,6 @@ def test_cross_origin_storage_warning_with_main_module(self): proc.stderr) def test_cross_origin_storage_warning_with_side_module(self): - """CROSS_ORIGIN_STORAGE + SIDE_MODULE must warn: no JS glue is emitted.""" proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sSIDE_MODULE', @@ -15705,7 +15670,6 @@ def test_cross_origin_storage_warning_with_side_module(self): proc.stderr) def test_cross_origin_storage_hash_changes_with_content(self): - """Two different programs must produce different embedded hashes.""" self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15723,15 +15687,7 @@ def test_cross_origin_storage_hash_changes_with_content(self): self.assertNotEqual(hash_a, hash_b, 'different programs should produce different embedded wasm hashes') - # --------------------------------------------------------------------------- - # Tests for CROSS_ORIGIN_STORAGE_ORIGINS - # --------------------------------------------------------------------------- - def test_cross_origin_storage_origins_default_is_global(self): - """Without -sCROSS_ORIGIN_STORAGE_ORIGINS, the default must be origins:'*'. - - Globally available; the user only needs -sCROSS_ORIGIN_STORAGE. - """ self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15739,10 +15695,6 @@ def test_cross_origin_storage_origins_default_is_global(self): self.assertContained("origins: '*'", read_file('hello.js')) def test_cross_origin_storage_origins_explicit_wildcard(self): - """Explicitly passing -sCROSS_ORIGIN_STORAGE_ORIGINS=['*'] must emit origins:'*'. - - This matches the implicit default. - """ self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15751,7 +15703,6 @@ def test_cross_origin_storage_origins_explicit_wildcard(self): self.assertContained("origins: '*'", read_file('hello.js')) def test_cross_origin_storage_origins_explicit_list(self): - """An explicit origins list must be emitted as a JS array.""" self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15763,19 +15714,16 @@ def test_cross_origin_storage_origins_explicit_list(self): self.assertNotContained("origins: '*'", js) def test_cross_origin_storage_origins_same_site(self): - """Empty origins list must omit the origins key entirely (same-site only).""" self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=[]', '-o', 'hello.js']) js = read_file('hello.js') - # { create: true } with no origins field self.assertContained('{ create: true }', js) self.assertNotContained('origins:', js) def test_cross_origin_storage_origins_error_mixed_wildcard(self): - """Mixing '*' with explicit origins must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', @@ -15784,7 +15732,6 @@ def test_cross_origin_storage_origins_error_mixed_wildcard(self): "'*' must not be mixed with explicit origins") def test_cross_origin_storage_origins_error_invalid_origin(self): - """A non-HTTPS or malformed origin must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', @@ -15793,7 +15740,6 @@ def test_cross_origin_storage_origins_error_invalid_origin(self): 'is not a valid HTTPS origin') def test_cross_origin_storage_origins_error_origin_with_path(self): - """An origin with a path component must be a link-time error.""" self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', @@ -15802,29 +15748,19 @@ def test_cross_origin_storage_origins_error_origin_with_path(self): 'is not a valid HTTPS origin') def test_cross_origin_storage_wasm_hash_module_property(self): - """Module['wasmHash'] must be set in the JS output and match the .wasm hash. - - Custom Module['instantiateWasm'] implementations bypass instantiateAsync() - and can read Module['wasmHash'] to get the build-time hash object. - """ self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') - - # The property must be present with a 64-char hex value. m = re.search(r"Module\['wasmHash'\]\s*=\s*\{[^}]*value:\s*'([0-9a-f]{64})'", js) self.assertTrue(m, "Module['wasmHash'] not found in JS output") embedded_hash = m.group(1) - - # It must equal the SHA-256 of the actual .wasm file. expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() self.assertEqual(embedded_hash, expected_hash, "Module['wasmHash'] does not match the actual .wasm SHA-256") def test_cross_origin_storage_wasm_hash_absent_without_flag(self): - """Module['wasmHash'] must NOT appear when CROSS_ORIGIN_STORAGE is off.""" self.run_process([EMCC, test_file('hello_world.c'), '-sENVIRONMENT=web', '-o', 'hello.js']) From 7b40c3ed94f42cf6a67f5ad690e7cbc52002ed61 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:43:19 +0200 Subject: [PATCH 56/87] refactor: extract setup_cross_origin_storage() from phase_linker_setup Per reviewer feedback (r3383144746) to help reduce the size of the already large phase_linker_setup function. --- tools/link.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/tools/link.py b/tools/link.py index f0ac0dd9ef1cc..645fd36993eaf 100644 --- a/tools/link.py +++ b/tools/link.py @@ -793,6 +793,25 @@ def get_dylibs(options, linker_args): return dylibs +def setup_cross_origin_storage(): + if not settings.ENVIRONMENT_MAY_BE_WEB: + exit_with_error('CROSS_ORIGIN_STORAGE requires a web environment (navigator.crossOriginStorage is not available outside the browser)') + if settings.SPLIT_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') + if settings.MAIN_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') + if settings.SIDE_MODULE: + diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') + origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS + if '*' in origins and len(origins) > 1: + exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") + for o in origins: + if o == '*': + continue + if not re.fullmatch(r'https://[^/]+(:\d+)?', o): + exit_with_error(f"CROSS_ORIGIN_STORAGE_ORIGINS: {o!r} is not a valid HTTPS origin (expected 'https://host' or 'https://host:port')") + + @ToolchainProfiler.profile_block('linker_setup') def phase_linker_setup(options, linker_args): # noqa: C901, PLR0912, PLR0915 """Future modifications should consider refactoring to reduce complexity. @@ -1210,24 +1229,7 @@ def limit_incoming_module_api(): exit_with_error('cannot have both WASM=2 and SINGLE_FILE enabled at the same time') if settings.CROSS_ORIGIN_STORAGE: - if not settings.ENVIRONMENT_MAY_BE_WEB: - exit_with_error('CROSS_ORIGIN_STORAGE requires a web environment (navigator.crossOriginStorage is not available outside the browser)') - if settings.SPLIT_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') - if settings.MAIN_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') - if settings.SIDE_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') - origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS - if '*' in origins and len(origins) > 1: - exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") - for o in origins: - if o == '*': - continue - # Each explicit origin must be a valid serialized HTTPS origin: - # scheme "https://", host, optional ":port", no path/query/fragment. - if not re.fullmatch(r'https://[^/]+(:\d+)?', o): - exit_with_error(f"CROSS_ORIGIN_STORAGE_ORIGINS: {o!r} is not a valid HTTPS origin (expected 'https://host' or 'https://host:port')") + setup_cross_origin_storage() if settings.MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION and options.oformat != OFormat.HTML: exit_with_error('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION is only compatible with html output') From b45ca742e9c2c6851dd00ea202bb238e5e0bb379 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:47:00 +0200 Subject: [PATCH 57/87] remove: COS example build artifact entries from .gitignore No test builds into test/cross_origin_storage/ and the Makefile has been removed, so these entries are no longer needed. Per reviewer feedback (r3383036408). --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 33e2179a20b3a..234517cc97797 100644 --- a/.gitignore +++ b/.gitignore @@ -30,10 +30,6 @@ coverage.xml # Test output /out/ -# COS example build artifacts -/test/cross_origin_storage/index.js -/test/cross_origin_storage/index.wasm - # When updating the website we check it out here. /site/emscripten-site/ From 14def98a19f7bfb65658c47476f96ec3bfc9d402 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 10:53:45 +0200 Subject: [PATCH 58/87] refactor: consolidate 19 COS unit tests into 5 - test_cross_origin_storage: core feature + disabled-by-default checks - test_cross_origin_storage_callbacks: opt-in via INCOMING_MODULE_JS_API - test_cross_origin_storage_warnings: SPLIT_MODULE + MAIN_MODULE + SIDE_MODULE - test_cross_origin_storage_errors: non-web + SINGLE_FILE + WASM_ASYNC=0 - test_cross_origin_storage_origins: explicit list + same-site + origin errors Drops redundant duplicate tests (origins_default, explicit_wildcard, wasm_hash_property, hash_changes_with_content). Per reviewer feedback. --- test/test_other.py | 111 ++++++++------------------------------------- 1 file changed, 20 insertions(+), 91 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 2c542a9df9dec..d7c2a74c277a6 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15577,8 +15577,7 @@ def test_deprecated_settings(self): err = self.run_process([EMCC, '-sUSE_PTHREADS', test_file('hello_world.c')], stderr=PIPE).stderr self.assertContained('emcc: warning: USE_PTHREADS is deprecated (prefer the standard -pthread flag). Please open a bug if you have a continuing need for this setting [-Wdeprecated]', err) - - def test_cross_origin_storage_js_output(self): + def test_cross_origin_storage(self): self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15601,8 +15600,14 @@ def test_cross_origin_storage_js_output(self): expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() self.assertEqual(embedded_hash, expected_hash, 'embedded wasm hash does not match actual .wasm SHA-256') + self.run_process([EMCC, test_file('hello_world.c'), + '-sENVIRONMENT=web', + '-o', 'hello.js']) + js = read_file('hello.js') + self.assertNotContained('crossOriginStorage', js) + self.assertNotContained("Module['wasmHash']", js) - def test_cross_origin_storage_callbacks_opt_in(self): + def test_cross_origin_storage_callbacks(self): self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15613,34 +15618,7 @@ def test_cross_origin_storage_callbacks_opt_in(self): self.assertContained("Module['onCOSCacheMiss']", js) self.assertContained("Module['onCOSStore']", js) - def test_cross_origin_storage_disabled_by_default(self): - self.run_process([EMCC, test_file('hello_world.c'), - '-sENVIRONMENT=web', - '-o', 'hello.js']) - js = read_file('hello.js') - self.assertNotContained('crossOriginStorage', js) - - def test_cross_origin_storage_error_for_non_web_target(self): - self.assert_fail([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=node'], - 'CROSS_ORIGIN_STORAGE requires a web environment') - - def test_cross_origin_storage_error_with_single_file(self): - self.assert_fail([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-sSINGLE_FILE'], - 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') - - def test_cross_origin_storage_error_without_async_compilation(self): - self.assert_fail([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-sWASM_ASYNC_COMPILATION=0'], - 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') - - def test_cross_origin_storage_warning_with_split_module(self): + def test_cross_origin_storage_warnings(self): proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15649,8 +15627,6 @@ def test_cross_origin_storage_warning_with_split_module(self): stderr=PIPE) self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', proc.stderr) - - def test_cross_origin_storage_warning_with_main_module(self): proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15659,8 +15635,6 @@ def test_cross_origin_storage_warning_with_main_module(self): stderr=PIPE) self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', proc.stderr) - - def test_cross_origin_storage_warning_with_side_module(self): proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sSIDE_MODULE', @@ -15669,40 +15643,23 @@ def test_cross_origin_storage_warning_with_side_module(self): self.assertContained('CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds', proc.stderr) - def test_cross_origin_storage_hash_changes_with_content(self): - self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-o', 'hello.js']) - js_a = read_file('hello.js') - hash_a = re.search(r"value:\s*'([0-9a-f]{64})'", js_a).group(1) - - self.run_process([EMCC, test_file('hello_world_small.c'), + def test_cross_origin_storage_errors(self): + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-o', 'small.js']) - js_b = read_file('small.js') - hash_b = re.search(r"value:\s*'([0-9a-f]{64})'", js_b).group(1) - - self.assertNotEqual(hash_a, hash_b, - 'different programs should produce different embedded wasm hashes') - - def test_cross_origin_storage_origins_default_is_global(self): - self.run_process([EMCC, test_file('hello_world.c'), + '-sENVIRONMENT=node'], + 'CROSS_ORIGIN_STORAGE requires a web environment') + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-o', 'hello.js']) - self.assertContained("origins: '*'", read_file('hello.js')) - - def test_cross_origin_storage_origins_explicit_wildcard(self): - self.run_process([EMCC, test_file('hello_world.c'), + '-sSINGLE_FILE'], + 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') + self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - "-sCROSS_ORIGIN_STORAGE_ORIGINS=['*']", - '-o', 'hello.js']) - self.assertContained("origins: '*'", read_file('hello.js')) + '-sWASM_ASYNC_COMPILATION=0'], + 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') - def test_cross_origin_storage_origins_explicit_list(self): + def test_cross_origin_storage_origins(self): self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15712,8 +15669,6 @@ def test_cross_origin_storage_origins_explicit_list(self): self.assertContained('"https://app.example.com"', js) self.assertContained('"https://api.example.com"', js) self.assertNotContained("origins: '*'", js) - - def test_cross_origin_storage_origins_same_site(self): self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', @@ -15722,24 +15677,18 @@ def test_cross_origin_storage_origins_same_site(self): js = read_file('hello.js') self.assertContained('{ create: true }', js) self.assertNotContained('origins:', js) - - def test_cross_origin_storage_origins_error_mixed_wildcard(self): self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]'], "'*' must not be mixed with explicit origins") - - def test_cross_origin_storage_origins_error_invalid_origin(self): self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]'], 'is not a valid HTTPS origin') - - def test_cross_origin_storage_origins_error_origin_with_path(self): self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', @@ -15747,26 +15696,6 @@ def test_cross_origin_storage_origins_error_origin_with_path(self): '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]'], 'is not a valid HTTPS origin') - def test_cross_origin_storage_wasm_hash_module_property(self): - self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-o', 'hello.js']) - js = read_file('hello.js') - m = re.search(r"Module\['wasmHash'\]\s*=\s*\{[^}]*value:\s*'([0-9a-f]{64})'", js) - self.assertTrue(m, "Module['wasmHash'] not found in JS output") - embedded_hash = m.group(1) - expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() - self.assertEqual(embedded_hash, expected_hash, - "Module['wasmHash'] does not match the actual .wasm SHA-256") - - def test_cross_origin_storage_wasm_hash_absent_without_flag(self): - self.run_process([EMCC, test_file('hello_world.c'), - '-sENVIRONMENT=web', - '-o', 'hello.js']) - js = read_file('hello.js') - self.assertNotContained("Module['wasmHash']", js) - def test_deprecated_settings(self): err = self.run_process([EMCC, '-sMEMORY64', test_file('hello_world.c')], stderr=PIPE).stderr self.assertContained('emcc: warning: MEMORY64 is deprecated (prefer the standard -m64 or --target=wasm64 flags). Please open a bug if you have a continuing need for this setting [-Wdeprecated]', err) From 73a0b0676286e3ef3468830fa3ceb80a9666973a Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:37:59 +0200 Subject: [PATCH 59/87] style: shorten COS ChangeLog entry to 3 lines --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 6d35d38420efc..36a441ba6ce34 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -27,6 +27,10 @@ See docs/process.md for more on how version tagging works. 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 `_ + as a progressive enhancement for Wasm loading on the web target. See + :ref:`CrossOriginStorage` for details. - 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 From fb3c1ee386dba20256bd1c9d57b509c57673de72 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:40:51 +0200 Subject: [PATCH 60/87] remove: test_cross_origin_storage_callbacks from test_other.py Browser functional tests cover this adequately. Per reviewer feedback. --- test/test_other.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index d7c2a74c277a6..2f35d457a65be 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15607,17 +15607,6 @@ def test_cross_origin_storage(self): self.assertNotContained('crossOriginStorage', js) self.assertNotContained("Module['wasmHash']", js) - def test_cross_origin_storage_callbacks(self): - self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-sINCOMING_MODULE_JS_API=onCOSCacheHit,onCOSCacheMiss,onCOSStore', - '-o', 'hello.js']) - js = read_file('hello.js') - self.assertContained("Module['onCOSCacheHit']", js) - self.assertContained("Module['onCOSCacheMiss']", js) - self.assertContained("Module['onCOSStore']", js) - def test_cross_origin_storage_warnings(self): proc = self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', From adba75eb18b21a28a17bb0601cd14895d3a5d36c Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:41:17 +0200 Subject: [PATCH 61/87] fix: copyright year and remove redundant build comment from main.cpp Build instructions belong in README.md, not the source file. Copyright year corrected to 2026. Per reviewer feedback. --- test/cross_origin_storage/main.cpp | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/test/cross_origin_storage/main.cpp b/test/cross_origin_storage/main.cpp index 946e88d7d9ed0..587f84682498e 100644 --- a/test/cross_origin_storage/main.cpp +++ b/test/cross_origin_storage/main.cpp @@ -1,22 +1,8 @@ -// Copyright 2025 The Emscripten Authors. All rights reserved. +// Copyright 2026 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. -// Example: Cross-Origin Storage (COS) integration -// -// Build with: -// emcc main.cpp -o index.js \ -// -O2 \ -// -sCROSS_ORIGIN_STORAGE \ -// -sENVIRONMENT=web \ -// -sEXPORTED_RUNTIME_METHODS=ccall \ -// -sEXPORTED_FUNCTIONS=_greet \ -// -sALLOW_MEMORY_GROWTH -// -// Serve the directory over HTTP (e.g. `emrun .` or `python3 -m http.server`) -// and open index.html in a browser that has the COS extension installed. - #include #include From b70e7cf2f8c5265df240afe29ec9b435a8ecb1ad Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:42:45 +0200 Subject: [PATCH 62/87] remove: test/cross_origin_storage/ example directory Not used by any automated test; browser tests use browser_test_hello_world.c directly. The directory caused repeated confusion and build artifact issues. Feature documentation lives in site/source/docs/compiling/CrossOriginStorage.rst. Per reviewer feedback. --- test/cross_origin_storage/README.md | 67 --------------------- test/cross_origin_storage/index.html | 89 ---------------------------- test/cross_origin_storage/main.cpp | 17 ------ 3 files changed, 173 deletions(-) delete mode 100644 test/cross_origin_storage/README.md delete mode 100644 test/cross_origin_storage/index.html delete mode 100644 test/cross_origin_storage/main.cpp diff --git a/test/cross_origin_storage/README.md b/test/cross_origin_storage/README.md deleted file mode 100644 index b289998c20a46..0000000000000 --- a/test/cross_origin_storage/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Cross-Origin Storage (COS) example - -This example demonstrates Emscripten's experimental -`-sCROSS_ORIGIN_STORAGE=1` flag, which integrates the -[Cross-Origin Storage browser API](https://github.com/WICG/cross-origin-storage) -into the standard Wasm loading path. - -## What it does - -COS is a **progressive enhancement**: when the browser exposes the -`navigator.crossOriginStorage` API, loading takes one of two paths: - -- **Cache miss** (first load): the `.wasm` module is fetched over the network - and stored in the cross-origin cache, keyed by its SHA-256 hash. -- **Cache hit** (subsequent loads, same or any other origin): the module is - retrieved from the cache without a network request for the binary. - -When the browser does not expose the COS API, or when an unexpected error -occurs, the runtime falls back to the standard `fetch()` / -`WebAssembly.instantiateStreaming()` path — the page always loads. - -The page reports which path was taken and, where applicable, the SHA-256 hash -of the Wasm resource and the URL it was fetched from. - -## Build - -```bash -emcc main.cpp -o index.js \ - -O2 \ - -sCROSS_ORIGIN_STORAGE \ - -sENVIRONMENT=web \ - -sEXPORTED_RUNTIME_METHODS=ccall \ - -sEXPORTED_FUNCTIONS=_greet \ - -sALLOW_MEMORY_GROWTH -``` - -This produces `index.js` and `index.wasm`. The SHA-256 hash of `index.wasm` -is embedded in `index.js` at build time — you can verify they match: - -```bash -sha256sum index.wasm -grep -oP "value: '\K[0-9a-f]{64}" index.js -``` - -## Run - -Serve the directory over HTTP (the `file://` protocol does not support -`fetch()`): - -```bash -emrun . -# or -python3 -m http.server -``` - -Open the page in a browser with the Cross-Origin Storage API available. - -The page will report: - -- whether the COS API is active -- on a cache miss: the URL the Wasm was fetched from, and confirmation once it has been stored in COS with its hash -- on a cache hit: the SHA-256 hash of the Wasm resource served from COS - -## See also - -- [COS Emscripten docs](../../site/source/docs/compiling/CrossOriginStorage.rst) -- [WICG explainer](https://github.com/WICG/cross-origin-storage) diff --git a/test/cross_origin_storage/index.html b/test/cross_origin_storage/index.html deleted file mode 100644 index 0ca1b7989453e..0000000000000 --- a/test/cross_origin_storage/index.html +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - Emscripten — Cross-Origin Storage example - - - -

Emscripten — Cross-Origin Storage example

-

Reload the page after the first load to observe the cache-hit path.

-
Loading…
- - - - - - - diff --git a/test/cross_origin_storage/main.cpp b/test/cross_origin_storage/main.cpp deleted file mode 100644 index 587f84682498e..0000000000000 --- a/test/cross_origin_storage/main.cpp +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2026 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. - -#include -#include - -// Called from JavaScript after the module loads. -extern "C" { - -EMSCRIPTEN_KEEPALIVE -const char* greet() { - return "Hello from WebAssembly!"; -} - -} // extern "C" From e738c9d3597c8acd7645fc76d368193d9febf428 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:51:50 +0200 Subject: [PATCH 63/87] Make feature detection rock-solid --- site/source/docs/compiling/CrossOriginStorage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 9a6dc4dfc7881..1498191479abf 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -328,7 +328,7 @@ via a reference to that config object: // `this` inside the callback is Emscripten's internal Module object; // read the hash via the outer Module reference instead. const cosHash = Module['wasmHash']; - if (cosHash?.value && 'crossOriginStorage' in navigator) { + if (cosHash?.value && 'navigator' in self && 'crossOriginStorage' in navigator) { navigator.crossOriginStorage.requestFileHandles([cosHash]) .then(handles => handles[0].getFile()) .then(f => f.arrayBuffer()) From 2edf04712e0dbdd614bb3e316c95876a6f020e29 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:55:33 +0200 Subject: [PATCH 64/87] Make feature detection compact --- site/source/docs/compiling/CrossOriginStorage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/source/docs/compiling/CrossOriginStorage.rst b/site/source/docs/compiling/CrossOriginStorage.rst index 1498191479abf..7e204a31b83f7 100644 --- a/site/source/docs/compiling/CrossOriginStorage.rst +++ b/site/source/docs/compiling/CrossOriginStorage.rst @@ -328,7 +328,7 @@ via a reference to that config object: // `this` inside the callback is Emscripten's internal Module object; // read the hash via the outer Module reference instead. const cosHash = Module['wasmHash']; - if (cosHash?.value && 'navigator' in self && 'crossOriginStorage' in navigator) { + if (cosHash?.value && globalThis.navigator?.crossOriginStorage) { navigator.crossOriginStorage.requestFileHandles([cosHash]) .then(handles => handles[0].getFile()) .then(f => f.arrayBuffer()) From 48cbac40bc292512f057d814d58a2cb7a5dbc73a Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 18:58:24 +0200 Subject: [PATCH 65/87] fix: use Markdown syntax in ChangeLog, not RST :ref: --- ChangeLog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 36a441ba6ce34..046df29e66f3d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -29,8 +29,10 @@ See docs/process.md for more on how version tagging works. 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 `_ +- 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 - :ref:`CrossOriginStorage` for details. + `docs/compiling/CrossOriginStorage.rst` for details. - 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 From dc085d98bb6d1d448ebd795f3b2a8398f14c9854 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 19:02:13 +0200 Subject: [PATCH 66/87] refactor: move COS incompatibilities to INCOMPATIBLE_SETTINGS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SIDE_MODULE is now a hard error (no JS glue emitted — genuinely incompatible). SPLIT_MODULE and MAIN_MODULE partial-coverage warnings are dropped; they are not true incompatibilities and add noise for an experimental feature. Per reviewer feedback (r3383144746). --- test/test_other.py | 29 ++++------------------------- tools/link.py | 6 ------ tools/settings.py | 1 + 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 2f35d457a65be..b4db51a0a7e3a 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15607,31 +15607,6 @@ def test_cross_origin_storage(self): self.assertNotContained('crossOriginStorage', js) self.assertNotContained("Module['wasmHash']", js) - def test_cross_origin_storage_warnings(self): - proc = self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-sSPLIT_MODULE', - '-o', 'hello.js'], - stderr=PIPE) - self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', - proc.stderr) - proc = self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-sMAIN_MODULE', - '-o', 'hello.js'], - stderr=PIPE) - self.assertContained('CROSS_ORIGIN_STORAGE only covers the primary .wasm file', - proc.stderr) - proc = self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sSIDE_MODULE', - '-o', 'hello.wasm'], - stderr=PIPE) - self.assertContained('CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds', - proc.stderr) - def test_cross_origin_storage_errors(self): self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', @@ -15647,6 +15622,10 @@ def test_cross_origin_storage_errors(self): '-sENVIRONMENT=web', '-sWASM_ASYNC_COMPILATION=0'], 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') + self.assert_fail([EMCC, test_file('hello_world.c'), + '-sCROSS_ORIGIN_STORAGE', + '-sSIDE_MODULE'], + 'CROSS_ORIGIN_STORAGE is not compatible with SIDE_MODULE') def test_cross_origin_storage_origins(self): self.run_process([EMCC, test_file('hello_world.c'), diff --git a/tools/link.py b/tools/link.py index 645fd36993eaf..3f24fc962fde9 100644 --- a/tools/link.py +++ b/tools/link.py @@ -796,12 +796,6 @@ def get_dylibs(options, linker_args): def setup_cross_origin_storage(): if not settings.ENVIRONMENT_MAY_BE_WEB: exit_with_error('CROSS_ORIGIN_STORAGE requires a web environment (navigator.crossOriginStorage is not available outside the browser)') - if settings.SPLIT_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; deferred split modules (.deferred.wasm) are fetched via the normal path and are not stored in or retrieved from COS') - if settings.MAIN_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE only covers the primary .wasm file; dynamically-linked side modules loaded via dlopen are fetched via the normal path and are not stored in or retrieved from COS') - if settings.SIDE_MODULE: - diagnostics.warning('emcc', 'CROSS_ORIGIN_STORAGE has no effect on SIDE_MODULE builds (no JS glue is emitted to carry the hash or perform the COS lookup)') origins = settings.CROSS_ORIGIN_STORAGE_ORIGINS if '*' in origins and len(origins) > 1: exit_with_error("CROSS_ORIGIN_STORAGE_ORIGINS: '*' must not be mixed with explicit origins") diff --git a/tools/settings.py b/tools/settings.py index 084537852b1d9..da7431dcfd0f2 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -152,6 +152,7 @@ ('CROSS_ORIGIN', 'NO_PTHREADS', None), ('CROSS_ORIGIN_STORAGE', 'SINGLE_FILE', 'the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on'), ('CROSS_ORIGIN_STORAGE', 'NO_WASM_ASYNC_COMPILATION', 'synchronous instantiation does not use the COS fetch path'), + ('CROSS_ORIGIN_STORAGE', 'SIDE_MODULE', 'no JS glue is emitted to carry the hash or perform the COS lookup'), ] EXPERIMENTAL_SETTINGS = { From 41b73f1f5dbc42e56594885b7f72821b7f724525 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 19:03:28 +0200 Subject: [PATCH 67/87] style: clean up COS browser tests - Drop -sENVIRONMENT=web (web is included by default) - Drop inline comment and docstrings - Drop section banner comment Per reviewer feedback. --- test/test_browser.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/test/test_browser.py b/test/test_browser.py index 3ee41b97c0a1e..3a0e24d02152a 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5699,27 +5699,14 @@ def test_pthread_memgrowth_stale_views(self): self.btest_exit('test_pthread_memgrowth_stale_views.c', cflags=['-pthread', '-sINITIAL_MEMORY=10mb', '-sALLOW_MEMORY_GROWTH', '-Wno-pthreads-mem-growth']) - # --------------------------------------------------------------------------- - # Cross-Origin Storage (COS) browser tests - # --------------------------------------------------------------------------- def test_cross_origin_storage_fallback(self): - """COS flag: page loads via the normal fetch path when COS API is absent.""" - # -sENVIRONMENT=web requires a real browser (not node). if not is_chrome(): self.skipTest('cross-origin storage tests require a Chromium-based browser') self.btest_exit('browser_test_hello_world.c', - cflags=['-O2', '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', '-Wno-experimental']) + cflags=['-O2', '-sCROSS_ORIGIN_STORAGE', '-Wno-experimental']) def test_cross_origin_storage_miss_then_hit(self): - """COS flag: first load triggers a cache-miss store; second load is a hit. - - Requires EMTEST_COS_EXTENSION_PATH to point to an unpacked copy of the - Cross-Origin Storage Chrome extension (manifest.json directory), which - polyfills navigator.crossOriginStorage. - - See: https://github.com/web-ai-community/cross-origin-storage-extension - """ if not is_chrome(): self.skipTest('cross-origin storage tests require a Chromium-based browser') if not EMTEST_COS_EXTENSION_PATH: @@ -5758,7 +5745,6 @@ def test_cross_origin_storage_miss_then_hit(self): self.compile_btest('browser_test_hello_world.c', [ '-O2', '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', '-Wno-experimental', '-sINCOMING_MODULE_JS_API=onAbort,onExit,onCOSStore,onCOSCacheHit', '--pre-js', 'cos_pre.js', From 76299606d8c7a08cf274e64bf0d641f86706e79f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 19:49:22 +0200 Subject: [PATCH 68/87] style: add PR number to COS ChangeLog entry --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 046df29e66f3d..63493ee841001 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -32,7 +32,7 @@ See docs/process.md for more on how version tagging works. - 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. + `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 From 7ae503e7acff6c8d36a26e26dc7ebd79c87f59f3 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 19:52:12 +0200 Subject: [PATCH 69/87] refactor: clean up COS preamble and hash injection - Use globalThis.navigator?.crossOriginStorage (matches codebase pattern) - Move var cosHash inside the if block - Hardcode 'SHA-256' in the template; drop <<< WASM_HASH_ALGORITHM >>> placeholder Per reviewer feedback. --- src/preamble.js | 6 +++--- test/test_other.py | 2 +- tools/link.py | 15 +++++---------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/preamble.js b/src/preamble.js index 201da8ab4e0f2..1719bf4496c5a 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -631,8 +631,8 @@ async function instantiateAsync(binary, binaryFile, imports) { // https://github.com/WICG/cross-origin-storage // Any error (NotAllowedError, network failure, …) falls through to the // standard Emscripten streaming path so the page always loads. - var cosHash = Module['wasmHash']; - if ('crossOriginStorage' in navigator) { + if (globalThis.navigator?.crossOriginStorage) { + var cosHash = Module['wasmHash']; try { var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); // Cache hit — read the Blob and instantiate from its ArrayBuffer. @@ -890,7 +890,7 @@ function getWasmImports() { #if CROSS_ORIGIN_STORAGE // Expose the build-time hash so that custom Module['instantiateWasm'] // callbacks can implement their own COS-aware loading path. - Module['wasmHash'] = { algorithm: '<<< WASM_HASH_ALGORITHM >>>', value: '<<< WASM_HASH_VALUE >>>' }; + Module['wasmHash'] = { algorithm: 'SHA-256', value: '<<< WASM_HASH_VALUE >>>' }; #endif #if expectToReceiveOnModule('instantiateWasm') diff --git a/test/test_other.py b/test/test_other.py index b4db51a0a7e3a..8410ccb75e30a 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15583,7 +15583,7 @@ def test_cross_origin_storage(self): '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') - self.assertContained("'crossOriginStorage' in navigator", js) + self.assertContained('globalThis.navigator?.crossOriginStorage', js) self.assertContained('navigator.crossOriginStorage.requestFileHandles', js) self.assertContained("algorithm: 'SHA-256'", js) self.assertContained("origins: '*'", js) diff --git a/tools/link.py b/tools/link.py index 3f24fc962fde9..1625e5e5f95eb 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1919,18 +1919,13 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat phase_binaryen(target, options, wasm_target) - # Compute the hash of the final wasm (after binaryen) and substitute the - # <<< WASM_HASH_ALGORITHM >>> / <<< WASM_HASH_VALUE >>> placeholders that - # preamble.js left in the generated JS. + # Compute the SHA-256 hash of the final wasm (after binaryen) and substitute + # the <<< WASM_HASH_VALUE >>> placeholder that preamble.js left in the JS. if final_js and settings.CROSS_ORIGIN_STORAGE: if os.path.exists(wasm_target): - wasm_bytes = utils.read_binary(wasm_target) - wasm_hash_algorithm = 'SHA-256' - wasm_hash_value = hashlib.sha256(wasm_bytes).hexdigest() - logger.debug(f'CROSS_ORIGIN_STORAGE: wasm {wasm_hash_algorithm} = {wasm_hash_value}') - js_content = read_file(final_js) - js_content = do_replace(js_content, '<<< WASM_HASH_ALGORITHM >>>', wasm_hash_algorithm) - js_content = do_replace(js_content, '<<< WASM_HASH_VALUE >>>', wasm_hash_value) + wasm_hash_value = hashlib.sha256(utils.read_binary(wasm_target)).hexdigest() + logger.debug(f'CROSS_ORIGIN_STORAGE: wasm SHA-256 = {wasm_hash_value}') + js_content = do_replace(read_file(final_js), '<<< WASM_HASH_VALUE >>>', wasm_hash_value) write_file(final_js, js_content) # If we are not emitting any JS then we are all done now From cf90bc1f723303b99ead45d319a2877b8cc1fe8b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 22:40:23 +0200 Subject: [PATCH 70/87] =?UTF-8?q?test:=20simplify=20COS=20assertions=20?= =?UTF-8?q?=E2=80=94=20use=20tighter=20algorithm+value=20regex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_other.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 8410ccb75e30a..aed0d262f40eb 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15583,18 +15583,7 @@ def test_cross_origin_storage(self): '-sENVIRONMENT=web', '-o', 'hello.js']) js = read_file('hello.js') - self.assertContained('globalThis.navigator?.crossOriginStorage', js) - self.assertContained('navigator.crossOriginStorage.requestFileHandles', js) - self.assertContained("algorithm: 'SHA-256'", js) - self.assertContained("origins: '*'", js) - self.assertContained('getFile()', js) - self.assertContained('createWritable()', js) - self.assertContained("'NotFoundError'", js) - self.assertContained("'NotAllowedError'", js) - self.assertNotContained("Module['onCOSCacheHit']", js) - self.assertNotContained("Module['onCOSCacheMiss']", js) - self.assertNotContained("Module['onCOSStore']", js) - m = re.search(r"value:\s*'([0-9a-f]{64})'", js) + m = re.search(r"algorithm:\s*'SHA-256',\s*value:\s*'([0-9a-f]{64})'", js) self.assertTrue(m, 'could not find a 64-char hex hash value in JS output') embedded_hash = m.group(1) expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() From 97d32e3f2025ea3ca000ea08a1bfe69a46a0fedc Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 22:41:37 +0200 Subject: [PATCH 71/87] test: use simplified CROSS_ORIGIN_STORAGE_ORIGINS syntax in test --- test/test_other.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index aed0d262f40eb..b38dbef467f3a 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15638,19 +15638,19 @@ def test_cross_origin_storage_origins(self): [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["*","https://example.com"]'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=*,https://example.com'], "'*' must not be mixed with explicit origins") self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["http://example.com"]'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=http://example.com'], 'is not a valid HTTPS origin') self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-sENVIRONMENT=web', - '-sCROSS_ORIGIN_STORAGE_ORIGINS=["https://example.com/path"]'], + '-sCROSS_ORIGIN_STORAGE_ORIGINS=https://example.com/path'], 'is not a valid HTTPS origin') def test_deprecated_settings(self): From 919501af2ac89fbe84c1a8a0e6a0658a23caef5f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 22:43:18 +0200 Subject: [PATCH 72/87] test: drop redundant -sENVIRONMENT=web from COS error tests (web is default) --- test/test_other.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index b38dbef467f3a..c00cf0214dda2 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15603,12 +15603,10 @@ def test_cross_origin_storage_errors(self): 'CROSS_ORIGIN_STORAGE requires a web environment') self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', '-sSINGLE_FILE'], 'CROSS_ORIGIN_STORAGE is not compatible with SINGLE_FILE') self.assert_fail([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', '-sWASM_ASYNC_COMPILATION=0'], 'CROSS_ORIGIN_STORAGE is not compatible with WASM_ASYNC_COMPILATION=0') self.assert_fail([EMCC, test_file('hello_world.c'), From c3ea9210303d82b447aac9f81d97cf33e6807014 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Wed, 10 Jun 2026 23:00:04 +0200 Subject: [PATCH 73/87] test: add TODO to remove COS extension once Chromium ships the feature natively --- test/browser_common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/browser_common.py b/test/browser_common.py index 522bc3cf42a23..7ce6694aaaf8a 100644 --- a/test/browser_common.py +++ b/test/browser_common.py @@ -72,6 +72,8 @@ # Point this at a local clone of: # https://github.com/web-ai-community/cross-origin-storage-extension # (the directory that contains manifest.json). +# TODO: Remove this once Chromium ships COS natively (even behind a flag), +# and update the browser test to use that flag instead. EMTEST_COS_EXTENSION_PATH = os.getenv('EMTEST_COS_EXTENSION_PATH', '') # Triggers the browser to restart after every given number of tests. From 0ad3f08fc3427e0f976967b23df92868ca3d84d9 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Fri, 12 Jun 2026 15:42:33 +0200 Subject: [PATCH 74/87] test: rebaseline codesize tests after upstream merge --- test/codesize/test_codesize_minimal_O0.expected.js | 3 +++ .../test_minimal_runtime_code_size_audio_worklet.json | 8 ++++---- .../test_minimal_runtime_code_size_hello_embind.json | 8 ++++---- .../test_minimal_runtime_code_size_hello_embind_val.json | 8 ++++---- .../test_minimal_runtime_code_size_hello_wasm_worker.json | 8 ++++---- .../test_minimal_runtime_code_size_hello_webgl2_wasm.json | 4 ++-- ...st_minimal_runtime_code_size_hello_webgl2_wasm2js.json | 8 ++++---- .../test_minimal_runtime_code_size_hello_webgl_wasm.json | 4 ++-- ...est_minimal_runtime_code_size_hello_webgl_wasm2js.json | 8 ++++---- ...test_minimal_runtime_code_size_random_printf_wasm.json | 4 ++-- ...t_minimal_runtime_code_size_random_printf_wasm2js.json | 4 ++-- 11 files changed, 35 insertions(+), 32 deletions(-) diff --git a/test/codesize/test_codesize_minimal_O0.expected.js b/test/codesize/test_codesize_minimal_O0.expected.js index e41cbbac2e0c9..5e6fbe3e6b888 100644 --- a/test/codesize/test_codesize_minimal_O0.expected.js +++ b/test/codesize/test_codesize_minimal_O0.expected.js @@ -1271,6 +1271,9 @@ function checkIncomingModuleAPI() { ignoredModuleProp('onRealloc'); ignoredModuleProp('onFree'); ignoredModuleProp('onSbrkGrow'); + ignoredModuleProp('onCOSCacheHit'); + ignoredModuleProp('onCOSCacheMiss'); + ignoredModuleProp('onCOSStore'); } // Imports from the Wasm binary. diff --git a/test/codesize/test_minimal_runtime_code_size_audio_worklet.json b/test/codesize/test_minimal_runtime_code_size_audio_worklet.json index f2789f2a8f778..7616a8ccd4012 100644 --- a/test/codesize/test_minimal_runtime_code_size_audio_worklet.json +++ b/test/codesize/test_minimal_runtime_code_size_audio_worklet.json @@ -3,8 +3,8 @@ "a.html.gz": 355, "a.js": 4647, "a.js.gz": 2381, - "a.wasm": 11237, - "a.wasm.gz": 6116, - "total": 16399, - "total_gz": 8852 + "a.wasm": 11239, + "a.wasm.gz": 6102, + "total": 16401, + "total_gz": 8838 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_embind.json b/test/codesize/test_minimal_runtime_code_size_hello_embind.json index 680ea781c39af..4e896f0db5494 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_embind.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_embind.json @@ -3,8 +3,8 @@ "a.html.gz": 371, "a.js": 7257, "a.js.gz": 3328, - "a.wasm": 7099, - "a.wasm.gz": 3246, - "total": 14904, - "total_gz": 6945 + "a.wasm": 7098, + "a.wasm.gz": 3244, + "total": 14903, + "total_gz": 6943 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json b/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json index af722695b21c4..ce7ed09d93889 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json @@ -3,8 +3,8 @@ "a.html.gz": 371, "a.js": 5354, "a.js.gz": 2519, - "a.wasm": 5741, - "a.wasm.gz": 2690, - "total": 11643, - "total_gz": 5580 + "a.wasm": 5740, + "a.wasm.gz": 2692, + "total": 11642, + "total_gz": 5582 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_wasm_worker.json b/test/codesize/test_minimal_runtime_code_size_hello_wasm_worker.json index 968786d7b6c5f..c3a3b1414fbc8 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_wasm_worker.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_wasm_worker.json @@ -3,8 +3,8 @@ "a.html.gz": 355, "a.js": 952, "a.js.gz": 598, - "a.wasm": 2661, - "a.wasm.gz": 1479, - "total": 4128, - "total_gz": 2432 + "a.wasm": 2649, + "a.wasm.gz": 1480, + "total": 4116, + "total_gz": 2433 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm.json b/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm.json index b796ae2e3afb9..e64c109c35a11 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm.json @@ -4,7 +4,7 @@ "a.js": 4398, "a.js.gz": 2259, "a.wasm": 8313, - "a.wasm.gz": 5646, + "a.wasm.gz": 5648, "total": 13161, - "total_gz": 8223 + "total_gz": 8225 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json b/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json index 08ef9b5c54814..24d73f3244840 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json @@ -1,8 +1,8 @@ { "a.html": 342, "a.html.gz": 252, - "a.js": 18192, - "a.js.gz": 9813, - "total": 18534, - "total_gz": 10065 + "a.js": 18189, + "a.js.gz": 9810, + "total": 18531, + "total_gz": 10062 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm.json b/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm.json index 9dc6d3a08181b..f20a31a512c6f 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm.json @@ -4,7 +4,7 @@ "a.js": 3936, "a.js.gz": 2095, "a.wasm": 8313, - "a.wasm.gz": 5646, + "a.wasm.gz": 5648, "total": 12699, - "total_gz": 8059 + "total_gz": 8061 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm2js.json b/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm2js.json index 88b024fee1321..cecef5eb6db01 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm2js.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm2js.json @@ -1,8 +1,8 @@ { "a.html": 342, "a.html.gz": 252, - "a.js": 17718, - "a.js.gz": 9647, - "total": 18060, - "total_gz": 9899 + "a.js": 17715, + "a.js.gz": 9643, + "total": 18057, + "total_gz": 9895 } diff --git a/test/codesize/test_minimal_runtime_code_size_random_printf_wasm.json b/test/codesize/test_minimal_runtime_code_size_random_printf_wasm.json index 66dd10f408dfa..53aacaaa564cd 100644 --- a/test/codesize/test_minimal_runtime_code_size_random_printf_wasm.json +++ b/test/codesize/test_minimal_runtime_code_size_random_printf_wasm.json @@ -1,4 +1,4 @@ { - "a.html": 11057, - "a.html.gz": 5756 + "a.html": 11052, + "a.html.gz": 5750 } diff --git a/test/codesize/test_minimal_runtime_code_size_random_printf_wasm2js.json b/test/codesize/test_minimal_runtime_code_size_random_printf_wasm2js.json index 5dc96cb01d779..2074c9212ec04 100644 --- a/test/codesize/test_minimal_runtime_code_size_random_printf_wasm2js.json +++ b/test/codesize/test_minimal_runtime_code_size_random_printf_wasm2js.json @@ -1,4 +1,4 @@ { - "a.html": 17417, - "a.html.gz": 7658 + "a.html": 17413, + "a.html.gz": 7650 } From dceaabb70f9cc0a1afad3e619c5e3365b5bd36d4 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Mon, 15 Jun 2026 10:47:32 +0200 Subject: [PATCH 75/87] refactor: use singular requestFileHandle() instead of requestFileHandles() See https://github.com/WICG/cross-origin-storage/issues/61 --- src/preamble.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/preamble.js b/src/preamble.js index 1719bf4496c5a..4ef1c63d9d8ab 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -634,9 +634,9 @@ async function instantiateAsync(binary, binaryFile, imports) { if (globalThis.navigator?.crossOriginStorage) { var cosHash = Module['wasmHash']; try { - var cosHandles = await navigator.crossOriginStorage.requestFileHandles([cosHash]); + var cosHandle = await navigator.crossOriginStorage.requestFileHandle(cosHash); // Cache hit — read the Blob and instantiate from its ArrayBuffer. - var cosFile = await cosHandles[0].getFile(); + var cosFile = await cosHandle.getFile(); var cosBytes = await cosFile.arrayBuffer(); #if expectToReceiveOnModule('onCOSCacheHit') Module['onCOSCacheHit']?.(cosHash.value); @@ -654,8 +654,8 @@ async function instantiateAsync(binary, binaryFile, imports) { // Fire-and-forget store; never block instantiation on the write. (async () => { try { - var writeHandles = await navigator.crossOriginStorage.requestFileHandles( - [cosHash], + var writeHandle = await navigator.crossOriginStorage.requestFileHandle( + cosHash, #if CROSS_ORIGIN_STORAGE_ORIGINS.length === 1 && CROSS_ORIGIN_STORAGE_ORIGINS[0] === '*' { create: true, origins: '*' }, #elif CROSS_ORIGIN_STORAGE_ORIGINS.length @@ -664,7 +664,7 @@ async function instantiateAsync(binary, binaryFile, imports) { { create: true }, #endif ); - var writable = await writeHandles[0].createWritable(); + var writable = await writeHandle.createWritable(); await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); await writable.close(); #if expectToReceiveOnModule('onCOSStore') From 82548a64df14b8306a075abd1219a61856cb9a27 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Tue, 16 Jun 2026 10:56:06 -0700 Subject: [PATCH 76/87] Apply suggestion from @tomayac --- tools/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/settings.py b/tools/settings.py index da7431dcfd0f2..fcdd6a2a419f1 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -160,7 +160,6 @@ 'SOURCE_PHASE_IMPORTS': '-sSOURCE_PHASE_IMPORTS is experimental and not yet supported in browsers', 'JS_BASE64_API': '-sJS_BASE64_API is experimental and not yet supported in browsers', 'CROSS_ORIGIN_STORAGE': '-sCROSS_ORIGIN_STORAGE is experimental; the underlying browser API is not yet shipped in any browser', - 'GROWABLE_ARRAYBUFFERS': '-sGROWABLE_ARRAYBUFFERS is still experimental and has only recently become available in browsers', 'SUPPORT_BIG_ENDIAN': '-sSUPPORT_BIG_ENDIAN is experimental, not all features are fully supported.', 'WASM_ESM_INTEGRATION': '-sWASM_ESM_INTEGRATION is still experimental and not yet supported in browsers', } From cb4f5b5aeec616aa34a6b4580ca08477b335c079 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Thu, 18 Jun 2026 17:49:02 +0200 Subject: [PATCH 77/87] fix: remove error-name dependency in COS catch block Collapse the NotFoundError / NotAllowedError / else branches into a single bare catch that always falls back to a network fetch and attempts a fire-and-forget store into COS. This makes the code robust against any future renaming of the error (see https://github.com/WICG/cross-origin-storage/issues/62). --- src/preamble.js | 63 +++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/src/preamble.js b/src/preamble.js index 4ef1c63d9d8ab..fd29e2a158aec 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -642,47 +642,42 @@ async function instantiateAsync(binary, binaryFile, imports) { Module['onCOSCacheHit']?.(cosHash.value); #endif return WebAssembly.instantiate(cosBytes, imports); - } catch (cosErr) { - if (cosErr.name === 'NotFoundError') { - // Cache miss — fetch normally, then store in COS for future consumers. - try { - var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); - var wasmBytes = await networkResponse.arrayBuffer(); + } catch { + // Any error (not found, not allowed, …) — fetch from the network and + // attempt to store in COS for future page loads. + try { + var networkResponse = await fetch(binaryFile, {{{ makeModuleReceiveExpr('fetchSettings', "{ credentials: 'same-origin' }") }}}); + var wasmBytes = await networkResponse.arrayBuffer(); #if expectToReceiveOnModule('onCOSCacheMiss') - Module['onCOSCacheMiss']?.(cosHash.value, binaryFile); + Module['onCOSCacheMiss']?.(cosHash.value, binaryFile); #endif - // Fire-and-forget store; never block instantiation on the write. - (async () => { - try { - var writeHandle = await navigator.crossOriginStorage.requestFileHandle( - cosHash, + // Fire-and-forget store; never block instantiation on the write. + (async () => { + try { + var writeHandle = await navigator.crossOriginStorage.requestFileHandle( + cosHash, #if CROSS_ORIGIN_STORAGE_ORIGINS.length === 1 && CROSS_ORIGIN_STORAGE_ORIGINS[0] === '*' - { create: true, origins: '*' }, + { create: true, origins: '*' }, #elif CROSS_ORIGIN_STORAGE_ORIGINS.length - { create: true, origins: {{{ JSON.stringify(CROSS_ORIGIN_STORAGE_ORIGINS) }}} }, + { create: true, origins: {{{ JSON.stringify(CROSS_ORIGIN_STORAGE_ORIGINS) }}} }, #else - { create: true }, + { create: true }, #endif - ); - var writable = await writeHandle.createWritable(); - await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); - await writable.close(); + ); + var writable = await writeHandle.createWritable(); + await writable.write(new Blob([wasmBytes], { type: 'application/wasm' })); + await writable.close(); #if expectToReceiveOnModule('onCOSStore') - Module['onCOSStore']?.(cosHash.value); -#endif - } catch (storeErr) { - err(`COS store failed: ${storeErr}`); - } - })(); - return WebAssembly.instantiate(wasmBytes, imports); - } catch (fetchErr) { - // Network fetch failed; fall through to the standard path below. - err(`COS fallback fetch failed: ${fetchErr}`); - } - } else if (cosErr.name === 'NotAllowedError') { - err(`COS: permission denied.`); - } else { - err(`Cross-Origin Storage lookup failed: ${cosErr}`); + Module['onCOSStore']?.(cosHash.value); +#endif + } catch (storeErr) { + err(`COS store failed: ${storeErr}`); + } + })(); + return WebAssembly.instantiate(wasmBytes, imports); + } catch (fetchErr) { + // Network fetch failed; fall through to the standard path below. + err(`COS fallback fetch failed: ${fetchErr}`); } // Fall through to the standard streaming path below. } From 8148660eb511085f0944270f8ae0295720df8e28 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Thu, 18 Jun 2026 17:53:34 +0200 Subject: [PATCH 78/87] docs: use abstract language in COS comment instead of concrete error names --- src/preamble.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/preamble.js b/src/preamble.js index fd29e2a158aec..83fe87436f76f 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -629,8 +629,8 @@ async function instantiateAsync(binary, binaryFile, imports) { #if CROSS_ORIGIN_STORAGE // Cross-Origin Storage (COS) progressive enhancement. // https://github.com/WICG/cross-origin-storage - // Any error (NotAllowedError, network failure, …) falls through to the - // standard Emscripten streaming path so the page always loads. + // Any error (not found, not allowed, network failure, …) falls through + // to the standard Emscripten streaming path so the page always loads. if (globalThis.navigator?.crossOriginStorage) { var cosHash = Module['wasmHash']; try { From 0ab7b138369890e2b83c38197934adc5de796272 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Thu, 18 Jun 2026 18:46:48 +0200 Subject: [PATCH 79/87] fix: resolve ruff lint errors introduced during rebase - Remove duplicate test_deprecated_settings definition in test_other.py - Fix extra blank line before test_cross_origin_storage_fallback in test_browser.py - Fix extra blank line after WASM_BINDGEN block in tools/link.py --- test/test_browser.py | 1 - test/test_other.py | 7 ------- tools/link.py | 1 - 3 files changed, 9 deletions(-) diff --git a/test/test_browser.py b/test/test_browser.py index 3a0e24d02152a..611e6fb2d6a0b 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5699,7 +5699,6 @@ def test_pthread_memgrowth_stale_views(self): self.btest_exit('test_pthread_memgrowth_stale_views.c', cflags=['-pthread', '-sINITIAL_MEMORY=10mb', '-sALLOW_MEMORY_GROWTH', '-Wno-pthreads-mem-growth']) - def test_cross_origin_storage_fallback(self): if not is_chrome(): self.skipTest('cross-origin storage tests require a Chromium-based browser') diff --git a/test/test_other.py b/test/test_other.py index c00cf0214dda2..58c306c2bac8f 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15650,10 +15650,3 @@ def test_cross_origin_storage_origins(self): '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=https://example.com/path'], 'is not a valid HTTPS origin') - - def test_deprecated_settings(self): - err = self.run_process([EMCC, '-sMEMORY64', test_file('hello_world.c')], stderr=PIPE).stderr - self.assertContained('emcc: warning: MEMORY64 is deprecated (prefer the standard -m64 or --target=wasm64 flags). Please open a bug if you have a continuing need for this setting [-Wdeprecated]', err) - - err = self.run_process([EMCC, '-sUSE_PTHREADS', test_file('hello_world.c')], stderr=PIPE).stderr - self.assertContained('emcc: warning: USE_PTHREADS is deprecated (prefer the standard -pthread flag). Please open a bug if you have a continuing need for this setting [-Wdeprecated]', err) diff --git a/tools/link.py b/tools/link.py index 1625e5e5f95eb..0a01a3bc77593 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1905,7 +1905,6 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat bindgen_jslib = building.run_wasm_bindgen(in_wasm) settings.JS_LIBRARIES.append(bindgen_jslib) - metadata = phase_emscript(in_wasm, wasm_target, js_syms, base_metadata) if settings.EMBIND_AOT: From 87f8973638c70f1fbc0a78e420ee61906cce964f Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Thu, 18 Jun 2026 18:55:17 +0200 Subject: [PATCH 80/87] Automatic rebaseline of codesize expectations. NFC This is an automatic change generated by tools/maint/rebaseline_tests.py. The following (24) test expectation files were updated by running the tests with `--rebaseline`: ``` codesize/test_codesize_cxx_ctors1.json: 151801 => 151847 [+46 bytes / +0.03%] codesize/test_codesize_cxx_ctors2.json: 151204 => 151250 [+46 bytes / +0.03%] codesize/test_codesize_cxx_except.json: 195695 => 195741 [+46 bytes / +0.02%] codesize/test_codesize_cxx_except_wasm.json: 166925 => 166971 [+46 bytes / +0.03%] codesize/test_codesize_cxx_except_wasm_legacy.json: 164809 => 164855 [+46 bytes / +0.03%] codesize/test_codesize_cxx_lto.json: 120628 => 120689 [+61 bytes / +0.05%] codesize/test_codesize_cxx_mangle.json: 262174 => 262220 [+46 bytes / +0.02%] codesize/test_codesize_cxx_noexcept.json: 153801 => 153847 [+46 bytes / +0.03%] codesize/test_codesize_cxx_wasmfs.json: 179676 => 179596 [-80 bytes / -0.04%] codesize/test_codesize_files_wasmfs.json: 63873 => 63677 [-196 bytes / -0.31%] codesize/test_codesize_hello_O0.json: 38629 => 38687 [+58 bytes / +0.15%] codesize/test_codesize_hello_dylink_all.json: 855713 => 855762 [+49 bytes / +0.01%] codesize/test_codesize_minimal_O0.json: 19682 => 19740 [+58 bytes / +0.29%] codesize/test_minimal_runtime_code_size_audio_worklet.json: 16401 => 16399 [-2 bytes / -0.01%] codesize/test_minimal_runtime_code_size_hello_embind.json: 14903 => 14904 [+1 bytes / +0.01%] codesize/test_minimal_runtime_code_size_hello_embind_val.json: 11642 => 11643 [+1 bytes / +0.01%] codesize/test_minimal_runtime_code_size_hello_wasm_worker.json: 4116 => 4128 [+12 bytes / +0.29%] codesize/test_minimal_runtime_code_size_hello_webgl2_wasm.json: 13161 => 13161 [+0 bytes / +0.00%] codesize/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json: 18531 => 18534 [+3 bytes / +0.02%] codesize/test_minimal_runtime_code_size_hello_webgl_wasm.json: 12699 => 12699 [+0 bytes / +0.00%] codesize/test_minimal_runtime_code_size_hello_webgl_wasm2js.json: 18057 => 18060 [+3 bytes / +0.02%] codesize/test_minimal_runtime_code_size_random_printf_wasm.json: 11052 => 11057 [+5 bytes / +0.05%] codesize/test_minimal_runtime_code_size_random_printf_wasm2js.json: 17413 => 17417 [+4 bytes / +0.02%] codesize/test_unoptimized_code_size.json: 176861 => 177085 [+224 bytes / +0.13%] Average change: +0.04% (-0.31% - +0.29%) ``` --- test/codesize/test_codesize_files_wasmfs.json | 8 ++++---- test/codesize/test_codesize_hello_O0.json | 8 ++++---- test/codesize/test_codesize_minimal_O0.json | 8 ++++---- ...test_minimal_runtime_code_size_audio_worklet.json | 8 ++++---- .../test_minimal_runtime_code_size_hello_embind.json | 8 ++++---- ...t_minimal_runtime_code_size_hello_embind_val.json | 8 ++++---- ..._minimal_runtime_code_size_hello_wasm_worker.json | 8 ++++---- ..._minimal_runtime_code_size_hello_webgl2_wasm.json | 4 ++-- ...nimal_runtime_code_size_hello_webgl2_wasm2js.json | 8 ++++---- ...t_minimal_runtime_code_size_hello_webgl_wasm.json | 4 ++-- ...inimal_runtime_code_size_hello_webgl_wasm2js.json | 8 ++++---- ...minimal_runtime_code_size_random_printf_wasm.json | 4 ++-- ...imal_runtime_code_size_random_printf_wasm2js.json | 4 ++-- test/codesize/test_unoptimized_code_size.json | 12 ++++++------ 14 files changed, 50 insertions(+), 50 deletions(-) diff --git a/test/codesize/test_codesize_files_wasmfs.json b/test/codesize/test_codesize_files_wasmfs.json index 6f75b38369c07..5095e9a9a3ffd 100644 --- a/test/codesize/test_codesize_files_wasmfs.json +++ b/test/codesize/test_codesize_files_wasmfs.json @@ -1,10 +1,10 @@ { "a.out.js": 5353, "a.out.js.gz": 2515, - "a.out.nodebug.wasm": 58520, - "a.out.nodebug.wasm.gz": 18225, - "total": 63873, - "total_gz": 20740, + "a.out.nodebug.wasm": 58324, + "a.out.nodebug.wasm.gz": 18215, + "total": 63677, + "total_gz": 20730, "sent": [ "a (emscripten_date_now)", "b (emscripten_err)", diff --git a/test/codesize/test_codesize_hello_O0.json b/test/codesize/test_codesize_hello_O0.json index 324537268f578..cda8320b130fe 100644 --- a/test/codesize/test_codesize_hello_O0.json +++ b/test/codesize/test_codesize_hello_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 23514, - "a.out.js.gz": 8493, + "a.out.js": 23572, + "a.out.js.gz": 8512, "a.out.nodebug.wasm": 15115, "a.out.nodebug.wasm.gz": 7464, - "total": 38629, - "total_gz": 15957, + "total": 38687, + "total_gz": 15976, "sent": [ "fd_write" ], diff --git a/test/codesize/test_codesize_minimal_O0.json b/test/codesize/test_codesize_minimal_O0.json index c842c84352bf7..e27d8b48cbc3d 100644 --- a/test/codesize/test_codesize_minimal_O0.json +++ b/test/codesize/test_codesize_minimal_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 18667, - "a.out.js.gz": 6759, + "a.out.js": 18725, + "a.out.js.gz": 6778, "a.out.nodebug.wasm": 1015, "a.out.nodebug.wasm.gz": 602, - "total": 19682, - "total_gz": 7361, + "total": 19740, + "total_gz": 7380, "sent": [], "imports": [], "exports": [ diff --git a/test/codesize/test_minimal_runtime_code_size_audio_worklet.json b/test/codesize/test_minimal_runtime_code_size_audio_worklet.json index 7616a8ccd4012..f2789f2a8f778 100644 --- a/test/codesize/test_minimal_runtime_code_size_audio_worklet.json +++ b/test/codesize/test_minimal_runtime_code_size_audio_worklet.json @@ -3,8 +3,8 @@ "a.html.gz": 355, "a.js": 4647, "a.js.gz": 2381, - "a.wasm": 11239, - "a.wasm.gz": 6102, - "total": 16401, - "total_gz": 8838 + "a.wasm": 11237, + "a.wasm.gz": 6116, + "total": 16399, + "total_gz": 8852 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_embind.json b/test/codesize/test_minimal_runtime_code_size_hello_embind.json index 4e896f0db5494..680ea781c39af 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_embind.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_embind.json @@ -3,8 +3,8 @@ "a.html.gz": 371, "a.js": 7257, "a.js.gz": 3328, - "a.wasm": 7098, - "a.wasm.gz": 3244, - "total": 14903, - "total_gz": 6943 + "a.wasm": 7099, + "a.wasm.gz": 3246, + "total": 14904, + "total_gz": 6945 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json b/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json index ce7ed09d93889..af722695b21c4 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_embind_val.json @@ -3,8 +3,8 @@ "a.html.gz": 371, "a.js": 5354, "a.js.gz": 2519, - "a.wasm": 5740, - "a.wasm.gz": 2692, - "total": 11642, - "total_gz": 5582 + "a.wasm": 5741, + "a.wasm.gz": 2690, + "total": 11643, + "total_gz": 5580 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_wasm_worker.json b/test/codesize/test_minimal_runtime_code_size_hello_wasm_worker.json index c3a3b1414fbc8..968786d7b6c5f 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_wasm_worker.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_wasm_worker.json @@ -3,8 +3,8 @@ "a.html.gz": 355, "a.js": 952, "a.js.gz": 598, - "a.wasm": 2649, - "a.wasm.gz": 1480, - "total": 4116, - "total_gz": 2433 + "a.wasm": 2661, + "a.wasm.gz": 1479, + "total": 4128, + "total_gz": 2432 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm.json b/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm.json index e64c109c35a11..b796ae2e3afb9 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm.json @@ -4,7 +4,7 @@ "a.js": 4398, "a.js.gz": 2259, "a.wasm": 8313, - "a.wasm.gz": 5648, + "a.wasm.gz": 5646, "total": 13161, - "total_gz": 8225 + "total_gz": 8223 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json b/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json index 24d73f3244840..08ef9b5c54814 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_webgl2_wasm2js.json @@ -1,8 +1,8 @@ { "a.html": 342, "a.html.gz": 252, - "a.js": 18189, - "a.js.gz": 9810, - "total": 18531, - "total_gz": 10062 + "a.js": 18192, + "a.js.gz": 9813, + "total": 18534, + "total_gz": 10065 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm.json b/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm.json index f20a31a512c6f..9dc6d3a08181b 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm.json @@ -4,7 +4,7 @@ "a.js": 3936, "a.js.gz": 2095, "a.wasm": 8313, - "a.wasm.gz": 5648, + "a.wasm.gz": 5646, "total": 12699, - "total_gz": 8061 + "total_gz": 8059 } diff --git a/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm2js.json b/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm2js.json index cecef5eb6db01..88b024fee1321 100644 --- a/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm2js.json +++ b/test/codesize/test_minimal_runtime_code_size_hello_webgl_wasm2js.json @@ -1,8 +1,8 @@ { "a.html": 342, "a.html.gz": 252, - "a.js": 17715, - "a.js.gz": 9643, - "total": 18057, - "total_gz": 9895 + "a.js": 17718, + "a.js.gz": 9647, + "total": 18060, + "total_gz": 9899 } diff --git a/test/codesize/test_minimal_runtime_code_size_random_printf_wasm.json b/test/codesize/test_minimal_runtime_code_size_random_printf_wasm.json index 53aacaaa564cd..66dd10f408dfa 100644 --- a/test/codesize/test_minimal_runtime_code_size_random_printf_wasm.json +++ b/test/codesize/test_minimal_runtime_code_size_random_printf_wasm.json @@ -1,4 +1,4 @@ { - "a.html": 11052, - "a.html.gz": 5750 + "a.html": 11057, + "a.html.gz": 5756 } diff --git a/test/codesize/test_minimal_runtime_code_size_random_printf_wasm2js.json b/test/codesize/test_minimal_runtime_code_size_random_printf_wasm2js.json index 2074c9212ec04..5dc96cb01d779 100644 --- a/test/codesize/test_minimal_runtime_code_size_random_printf_wasm2js.json +++ b/test/codesize/test_minimal_runtime_code_size_random_printf_wasm2js.json @@ -1,4 +1,4 @@ { - "a.html": 17413, - "a.html.gz": 7650 + "a.html": 17417, + "a.html.gz": 7658 } diff --git a/test/codesize/test_unoptimized_code_size.json b/test/codesize/test_unoptimized_code_size.json index d7fc8d770e642..214e66be63154 100644 --- a/test/codesize/test_unoptimized_code_size.json +++ b/test/codesize/test_unoptimized_code_size.json @@ -1,16 +1,16 @@ { - "hello_world.js": 55387, - "hello_world.js.gz": 17453, + "hello_world.js": 55499, + "hello_world.js.gz": 17473, "hello_world.wasm": 15115, "hello_world.wasm.gz": 7464, "no_asserts.js": 25871, "no_asserts.js.gz": 8759, "no_asserts.wasm": 12229, "no_asserts.wasm.gz": 6004, - "strict.js": 53144, - "strict.js.gz": 16681, + "strict.js": 53256, + "strict.js.gz": 16714, "strict.wasm": 15115, "strict.wasm.gz": 7461, - "total": 176861, - "total_gz": 63822 + "total": 177085, + "total_gz": 63875 } From 722377bc52c2f0a52f7d250921da279054bb6d01 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Thu, 18 Jun 2026 18:58:01 +0200 Subject: [PATCH 81/87] fix: address reviewer feedback on ChangeLog, settings.js, and preamble.js - ChangeLog.md: remove duplicate RST-format entry and bad-merge artifact; keep single clean Markdown entry for -sCROSS_ORIGIN_STORAGE - src/settings.js: reflow CROSS_ORIGIN_STORAGE comment so first line is not shorter than the rest - src/preamble.js: drop redundant .length === 1 check in origins #if (Python link-time validation already prevents '*' from being mixed with explicit origins, so [0] === '*' implies length === 1) --- ChangeLog.md | 20 -------------------- src/preamble.js | 2 +- src/settings.js | 14 +++++++------- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 63493ee841001..91d84a0c3d28a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -27,8 +27,6 @@ See docs/process.md for more on how version tagging works. 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 `_ - 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 @@ -41,24 +39,6 @@ See docs/process.md for more on how version tagging works. supported. (#23493) - Fixed `getentropy`/`random_get` spuriously failing under Node.js and the shell environment for small requests. (#27122) -- New experimental ``-sCROSS_ORIGIN_STORAGE=1`` linker flag that integrates -- New experimental ``-sCROSS_ORIGIN_STORAGE`` linker flag that integrates - the proposed `Cross-Origin Storage browser API - `_ into the Wasm loading path - as a progressive enhancement (web target only). At build time Emscripten - computes the SHA-256 hash of the final ``.wasm`` binary and embeds it in the - generated JS glue. At runtime, if the browser exposes - ``navigator.crossOriginStorage``, the runtime first attempts to retrieve the - module from the shared cross-origin cache (cache hit); on a miss it fetches - normally and stores the binary in COS for future use by any origin. Falls - back transparently to the standard fetch path when the API is unavailable. - Three optional ``Module`` callbacks are available for instrumentation: - ``Module['onCOSCacheHit'](hash)``, ``Module['onCOSCacheMiss'](hash, url)``, - and ``Module['onCOSStore'](hash)``. Incompatible with ``-sSINGLE_FILE`` - and ``-sWASM_ASYNC_COMPILATION=0`` (both produce hard link-time errors). - The companion ``-sCROSS_ORIGIN_STORAGE_ORIGINS`` setting controls which - origins may read the cached file: ``['*']`` (default, globally available), - an explicit HTTPS origin list (restricted), or ``[]`` (same-site only). 6.0.0 - 06/04/26 ---------------- diff --git a/src/preamble.js b/src/preamble.js index 83fe87436f76f..c95e10b85b09f 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -656,7 +656,7 @@ async function instantiateAsync(binary, binaryFile, imports) { try { var writeHandle = await navigator.crossOriginStorage.requestFileHandle( cosHash, -#if CROSS_ORIGIN_STORAGE_ORIGINS.length === 1 && CROSS_ORIGIN_STORAGE_ORIGINS[0] === '*' +#if CROSS_ORIGIN_STORAGE_ORIGINS[0] === '*' { create: true, origins: '*' }, #elif CROSS_ORIGIN_STORAGE_ORIGINS.length { create: true, origins: {{{ JSON.stringify(CROSS_ORIGIN_STORAGE_ORIGINS) }}} }, diff --git a/src/settings.js b/src/settings.js index 5d5b91d05982a..8d8de38121c19 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2205,13 +2205,13 @@ var GROWABLE_ARRAYBUFFERS = false; // indirectly using `importScripts` var CROSS_ORIGIN = false; -// Enables Cross-Origin Storage (COS) API support for Wasm -// loading on the Web target. At link time Emscripten computes the SHA-256 -// hash of the final ``.wasm`` binary and embeds it in the generated JS. -// At runtime the COS API is used as a progressive enhancement: the binary is -// fetched from the shared cross-origin cache on a hit, or stored there after -// a network fetch on a miss; when the API is absent or errors the runtime -// falls through to the standard fetch path. +// Enables Cross-Origin Storage (COS) API support for Wasm loading on the +// Web target. At link time Emscripten computes the SHA-256 hash of the +// final ``.wasm`` binary and embeds it in the generated JS. At runtime the +// COS API is used as a progressive enhancement: the binary is fetched from +// the shared cross-origin cache on a hit, or stored there after a network +// fetch on a miss; when the API is absent or errors the runtime falls +// through to the standard fetch path. // // Requires the Web environment; using it without ``-sENVIRONMENT=web`` is a // hard link-time error. Incompatible with SINGLE_FILE and From f159572708b4026b1c3c24aec870a8043f7a4832 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Thu, 18 Jun 2026 19:01:24 +0200 Subject: [PATCH 82/87] docs: regenerate settings_reference.rst after comment reflow --- .../docs/tools_reference/settings_reference.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index dbd3537c5a6ed..ce2494edbd483 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3365,13 +3365,13 @@ Default value: false CROSS_ORIGIN_STORAGE ==================== -Enables Cross-Origin Storage (COS) API support for Wasm -loading on the Web target. At link time Emscripten computes the SHA-256 -hash of the final ``.wasm`` binary and embeds it in the generated JS. -At runtime the COS API is used as a progressive enhancement: the binary is -fetched from the shared cross-origin cache on a hit, or stored there after -a network fetch on a miss; when the API is absent or errors the runtime -falls through to the standard fetch path. +Enables Cross-Origin Storage (COS) API support for Wasm loading on the +Web target. At link time Emscripten computes the SHA-256 hash of the +final ``.wasm`` binary and embeds it in the generated JS. At runtime the +COS API is used as a progressive enhancement: the binary is fetched from +the shared cross-origin cache on a hit, or stored there after a network +fetch on a miss; when the API is absent or errors the runtime falls +through to the standard fetch path. Requires the Web environment; using it without ``-sENVIRONMENT=web`` is a hard link-time error. Incompatible with SINGLE_FILE and From f5ef2fe4b9a67f61b67b452153b3bcebc68cffa1 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Thu, 18 Jun 2026 19:02:37 +0200 Subject: [PATCH 83/87] fix: remove os.path.exists guard in COS hash computation If CROSS_ORIGIN_STORAGE is set and we reach this point in the link but wasm_target does not exist, crashing is the correct behavior rather than silently skipping the hash substitution. --- tools/link.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tools/link.py b/tools/link.py index 0a01a3bc77593..0a01e7544c008 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1921,11 +1921,10 @@ def phase_post_link(options, in_wasm, wasm_target, target, js_syms, base_metadat # Compute the SHA-256 hash of the final wasm (after binaryen) and substitute # the <<< WASM_HASH_VALUE >>> placeholder that preamble.js left in the JS. if final_js and settings.CROSS_ORIGIN_STORAGE: - if os.path.exists(wasm_target): - wasm_hash_value = hashlib.sha256(utils.read_binary(wasm_target)).hexdigest() - logger.debug(f'CROSS_ORIGIN_STORAGE: wasm SHA-256 = {wasm_hash_value}') - js_content = do_replace(read_file(final_js), '<<< WASM_HASH_VALUE >>>', wasm_hash_value) - write_file(final_js, js_content) + wasm_hash_value = hashlib.sha256(utils.read_binary(wasm_target)).hexdigest() + logger.debug(f'CROSS_ORIGIN_STORAGE: wasm SHA-256 = {wasm_hash_value}') + js_content = do_replace(read_file(final_js), '<<< WASM_HASH_VALUE >>>', wasm_hash_value) + write_file(final_js, js_content) # If we are not emitting any JS then we are all done now if options.oformat != OFormat.WASM: From f7a6dbd407788bb41d363872a52e63bd6b5e22f1 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Thu, 18 Jun 2026 19:05:05 +0200 Subject: [PATCH 84/87] test: drop redundant -sENVIRONMENT=web from origins assert_fail tests Default ENVIRONMENT is empty which means ENVIRONMENT_MAY_BE_WEB=True, so the explicit flag adds no value in these error-path tests. --- test/test_other.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 58c306c2bac8f..60cdcdc132546 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15635,18 +15635,15 @@ def test_cross_origin_storage_origins(self): self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=*,https://example.com'], "'*' must not be mixed with explicit origins") self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=http://example.com'], 'is not a valid HTTPS origin') self.assert_fail( [EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=https://example.com/path'], 'is not a valid HTTPS origin') From eda32d9a684a2ec0a3f33c6bffab018346f201d3 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Thu, 18 Jun 2026 19:07:04 +0200 Subject: [PATCH 85/87] test: drop -sENVIRONMENT=web from COS tests and condense single-flag cmds Default ENVIRONMENT already includes web, so the flag is redundant. Condense the two single-flag run_process calls into one line each. --- test/test_other.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 60cdcdc132546..3611832c466c3 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -15578,10 +15578,7 @@ def test_deprecated_settings(self): self.assertContained('emcc: warning: USE_PTHREADS is deprecated (prefer the standard -pthread flag). Please open a bug if you have a continuing need for this setting [-Wdeprecated]', err) def test_cross_origin_storage(self): - self.run_process([EMCC, test_file('hello_world.c'), - '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', - '-o', 'hello.js']) + self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', '-o', 'hello.js']) js = read_file('hello.js') m = re.search(r"algorithm:\s*'SHA-256',\s*value:\s*'([0-9a-f]{64})'", js) self.assertTrue(m, 'could not find a 64-char hex hash value in JS output') @@ -15589,9 +15586,7 @@ def test_cross_origin_storage(self): expected_hash = hashlib.sha256(open('hello.wasm', 'rb').read()).hexdigest() self.assertEqual(embedded_hash, expected_hash, 'embedded wasm hash does not match actual .wasm SHA-256') - self.run_process([EMCC, test_file('hello_world.c'), - '-sENVIRONMENT=web', - '-o', 'hello.js']) + self.run_process([EMCC, test_file('hello_world.c'), '-o', 'hello.js']) js = read_file('hello.js') self.assertNotContained('crossOriginStorage', js) self.assertNotContained("Module['wasmHash']", js) @@ -15617,7 +15612,6 @@ def test_cross_origin_storage_errors(self): def test_cross_origin_storage_origins(self): self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com', '-o', 'hello.js']) js = read_file('hello.js') @@ -15626,7 +15620,6 @@ def test_cross_origin_storage_origins(self): self.assertNotContained("origins: '*'", js) self.run_process([EMCC, test_file('hello_world.c'), '-sCROSS_ORIGIN_STORAGE', - '-sENVIRONMENT=web', '-sCROSS_ORIGIN_STORAGE_ORIGINS=[]', '-o', 'hello.js']) js = read_file('hello.js') From 57e1ec45aa6294fc8ca9f4cf418bab993dc72d25 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Thu, 18 Jun 2026 19:49:08 +0200 Subject: [PATCH 86/87] Automatic rebaseline of codesize expectations. NFC This is an automatic change generated by tools/maint/rebaseline_tests.py. The following (2) test expectation files were updated by running the tests with `--rebaseline`: ``` codesize/test_codesize_cxx_wasmfs.json: 179722 => 179596 [-126 bytes / -0.07%] codesize/test_codesize_hello_dylink_all.json: 855768 => 855762 [-6 bytes / -0.00%] Average change: -0.04% (-0.07% - -0.00%) ``` --- test/codesize/test_codesize_cxx_wasmfs.json | 8 ++++---- test/codesize/test_codesize_hello_dylink_all.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/codesize/test_codesize_cxx_wasmfs.json b/test/codesize/test_codesize_cxx_wasmfs.json index 4521082b1348d..c03def3c6502a 100644 --- a/test/codesize/test_codesize_cxx_wasmfs.json +++ b/test/codesize/test_codesize_cxx_wasmfs.json @@ -1,10 +1,10 @@ { "a.out.js": 6919, "a.out.js.gz": 3244, - "a.out.nodebug.wasm": 172803, - "a.out.nodebug.wasm.gz": 63374, - "total": 179722, - "total_gz": 66618, + "a.out.nodebug.wasm": 172677, + "a.out.nodebug.wasm.gz": 63357, + "total": 179596, + "total_gz": 66601, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 9846bd74e34a3..fa02be0975aab 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { "a.out.js": 268210, - "a.out.nodebug.wasm": 587558, - "total": 855768, + "a.out.nodebug.wasm": 587552, + "total": 855762, "sent": [ "IMG_Init", "IMG_Load", From f1105ebdcf73d145c4a17b57b22fdad1e4d3d1b6 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Thu, 18 Jun 2026 15:21:53 -0700 Subject: [PATCH 87/87] Rebaseline --- test/codesize/test_codesize_cxx_ctors1.json | 8 +++---- test/codesize/test_codesize_cxx_ctors2.json | 8 +++---- test/codesize/test_codesize_cxx_except.json | 8 +++---- .../test_codesize_cxx_except_wasm.json | 8 +++---- .../test_codesize_cxx_except_wasm_legacy.json | 8 +++---- test/codesize/test_codesize_cxx_lto.json | 8 +++---- test/codesize/test_codesize_cxx_mangle.json | 8 +++---- test/codesize/test_codesize_cxx_noexcept.json | 8 +++---- test/codesize/test_codesize_cxx_wasmfs.json | 12 +++++----- .../test_codesize_file_preload.expected.js | 24 +++++++------------ test/codesize/test_codesize_file_preload.json | 8 +++---- test/codesize/test_codesize_files_js_fs.json | 8 +++---- test/codesize/test_codesize_files_wasmfs.json | 12 +++++----- test/codesize/test_codesize_hello_O0.json | 8 +++---- test/codesize/test_codesize_hello_O1.json | 8 +++---- test/codesize/test_codesize_hello_O2.json | 8 +++---- test/codesize/test_codesize_hello_O3.json | 8 +++---- test/codesize/test_codesize_hello_Os.json | 8 +++---- test/codesize/test_codesize_hello_Oz.json | 8 +++---- test/codesize/test_codesize_hello_dylink.json | 8 +++---- .../test_codesize_hello_dylink_all.json | 8 ++++--- .../test_codesize_hello_export_nothing.json | 8 +++---- .../test_codesize_hello_single_file.json | 4 ++-- test/codesize/test_codesize_hello_wasmfs.json | 8 +++---- .../test_codesize_libcxxabi_message_O3.json | 8 +++---- ...esize_libcxxabi_message_O3_standalone.json | 8 +++---- test/codesize/test_codesize_mem_O3.json | 4 ++-- test/codesize/test_codesize_mem_O3_grow.json | 4 ++-- .../test_codesize_mem_O3_grow_standalone.json | 8 +++---- .../test_codesize_mem_O3_standalone.json | 8 +++---- .../test_codesize_mem_O3_standalone_lib.json | 8 +++---- .../test_codesize_mem_O3_standalone_narg.json | 8 +++---- ..._codesize_mem_O3_standalone_narg_flto.json | 8 +++---- .../test_codesize_minimal_O0.expected.js | 20 +++++----------- test/codesize/test_codesize_minimal_O0.json | 4 ++-- test/codesize/test_small_js_flags.expected.js | 19 +++++---------- test/codesize/test_unoptimized_code_size.json | 16 ++++++------- 37 files changed, 157 insertions(+), 178 deletions(-) diff --git a/test/codesize/test_codesize_cxx_ctors1.json b/test/codesize/test_codesize_cxx_ctors1.json index 1db9088196bd7..29f5046bf3832 100644 --- a/test/codesize/test_codesize_cxx_ctors1.json +++ b/test/codesize/test_codesize_cxx_ctors1.json @@ -1,10 +1,10 @@ { - "a.out.js": 19181, - "a.out.js.gz": 7935, + "a.out.js": 19218, + "a.out.js.gz": 7948, "a.out.nodebug.wasm": 132666, "a.out.nodebug.wasm.gz": 49962, - "total": 151847, - "total_gz": 57897, + "total": 151884, + "total_gz": 57910, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_ctors2.json b/test/codesize/test_codesize_cxx_ctors2.json index 0916b14859838..2c2b1f345bd1a 100644 --- a/test/codesize/test_codesize_cxx_ctors2.json +++ b/test/codesize/test_codesize_cxx_ctors2.json @@ -1,10 +1,10 @@ { - "a.out.js": 19158, - "a.out.js.gz": 7919, + "a.out.js": 19195, + "a.out.js.gz": 7932, "a.out.nodebug.wasm": 132092, "a.out.nodebug.wasm.gz": 49625, - "total": 151250, - "total_gz": 57544, + "total": 151287, + "total_gz": 57557, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_except.json b/test/codesize/test_codesize_cxx_except.json index 38f651cf51b5a..f65ce09f2fa91 100644 --- a/test/codesize/test_codesize_cxx_except.json +++ b/test/codesize/test_codesize_cxx_except.json @@ -1,10 +1,10 @@ { - "a.out.js": 23155, - "a.out.js.gz": 8922, + "a.out.js": 23191, + "a.out.js.gz": 8939, "a.out.nodebug.wasm": 172586, "a.out.nodebug.wasm.gz": 57501, - "total": 195741, - "total_gz": 66423, + "total": 195777, + "total_gz": 66440, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_except_wasm.json b/test/codesize/test_codesize_cxx_except_wasm.json index b0c81ad251e0b..ea0a4d4448010 100644 --- a/test/codesize/test_codesize_cxx_except_wasm.json +++ b/test/codesize/test_codesize_cxx_except_wasm.json @@ -1,10 +1,10 @@ { - "a.out.js": 18980, - "a.out.js.gz": 7854, + "a.out.js": 19017, + "a.out.js.gz": 7872, "a.out.nodebug.wasm": 147991, "a.out.nodebug.wasm.gz": 55370, - "total": 166971, - "total_gz": 63224, + "total": 167008, + "total_gz": 63242, "sent": [ "_abort_js", "_tzset_js", diff --git a/test/codesize/test_codesize_cxx_except_wasm_legacy.json b/test/codesize/test_codesize_cxx_except_wasm_legacy.json index b4f3ecab86584..d420fd8a4cad7 100644 --- a/test/codesize/test_codesize_cxx_except_wasm_legacy.json +++ b/test/codesize/test_codesize_cxx_except_wasm_legacy.json @@ -1,10 +1,10 @@ { - "a.out.js": 19058, - "a.out.js.gz": 7878, + "a.out.js": 19095, + "a.out.js.gz": 7894, "a.out.nodebug.wasm": 145797, "a.out.nodebug.wasm.gz": 54992, - "total": 164855, - "total_gz": 62870, + "total": 164892, + "total_gz": 62886, "sent": [ "_abort_js", "_tzset_js", diff --git a/test/codesize/test_codesize_cxx_lto.json b/test/codesize/test_codesize_cxx_lto.json index 8152afc490b3c..5a3d882c08153 100644 --- a/test/codesize/test_codesize_cxx_lto.json +++ b/test/codesize/test_codesize_cxx_lto.json @@ -1,10 +1,10 @@ { - "a.out.js": 18521, - "a.out.js.gz": 7624, + "a.out.js": 18557, + "a.out.js.gz": 7638, "a.out.nodebug.wasm": 102168, "a.out.nodebug.wasm.gz": 39572, - "total": 120689, - "total_gz": 47196, + "total": 120725, + "total_gz": 47210, "sent": [ "a (emscripten_resize_heap)", "b (_setitimer_js)", diff --git a/test/codesize/test_codesize_cxx_mangle.json b/test/codesize/test_codesize_cxx_mangle.json index 1d0749aba8745..5164bca8724e6 100644 --- a/test/codesize/test_codesize_cxx_mangle.json +++ b/test/codesize/test_codesize_cxx_mangle.json @@ -1,10 +1,10 @@ { - "a.out.js": 23205, - "a.out.js.gz": 8945, + "a.out.js": 23241, + "a.out.js.gz": 8961, "a.out.nodebug.wasm": 239015, "a.out.nodebug.wasm.gz": 79854, - "total": 262220, - "total_gz": 88799, + "total": 262256, + "total_gz": 88815, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_noexcept.json b/test/codesize/test_codesize_cxx_noexcept.json index 17393849e0492..b5f7b1f755279 100644 --- a/test/codesize/test_codesize_cxx_noexcept.json +++ b/test/codesize/test_codesize_cxx_noexcept.json @@ -1,10 +1,10 @@ { - "a.out.js": 19181, - "a.out.js.gz": 7935, + "a.out.js": 19218, + "a.out.js.gz": 7948, "a.out.nodebug.wasm": 134666, "a.out.nodebug.wasm.gz": 50806, - "total": 153847, - "total_gz": 58741, + "total": 153884, + "total_gz": 58754, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_wasmfs.json b/test/codesize/test_codesize_cxx_wasmfs.json index c03def3c6502a..3fd32b3e1faf4 100644 --- a/test/codesize/test_codesize_cxx_wasmfs.json +++ b/test/codesize/test_codesize_cxx_wasmfs.json @@ -1,10 +1,10 @@ { - "a.out.js": 6919, - "a.out.js.gz": 3244, - "a.out.nodebug.wasm": 172677, - "a.out.nodebug.wasm.gz": 63357, - "total": 179596, - "total_gz": 66601, + "a.out.js": 6938, + "a.out.js.gz": 3259, + "a.out.nodebug.wasm": 172803, + "a.out.nodebug.wasm.gz": 63374, + "total": 179741, + "total_gz": 66633, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_file_preload.expected.js b/test/codesize/test_codesize_file_preload.expected.js index 0c3dcdb11ede6..e480e750f3c58 100644 --- a/test/codesize/test_codesize_file_preload.expected.js +++ b/test/codesize/test_codesize_file_preload.expected.js @@ -3178,25 +3178,17 @@ function callMain() { } } -function run() { +async function run() { preRun(); if (runDependencies > 0) { - dependenciesFulfilled = run; - return; - } - function doRun() { - // run may have just been called through dependencies being fulfilled just in this very frame, - // or while the async setStatus time below was happening - if (ABORT) return; - initRuntime(); - preMain(); - var noInitialRun = false; - if (!noInitialRun) callMain(); - postRun(); - } - { - doRun(); + await new Promise(resolve => dependenciesFulfilled = resolve); } + if (ABORT) return; + initRuntime(); + preMain(); + var noInitialRun = false; + if (!noInitialRun) callMain(); + postRun(); } var wasmExports; diff --git a/test/codesize/test_codesize_file_preload.json b/test/codesize/test_codesize_file_preload.json index 75202b478ab4f..26a8db46e27eb 100644 --- a/test/codesize/test_codesize_file_preload.json +++ b/test/codesize/test_codesize_file_preload.json @@ -1,10 +1,10 @@ { - "a.out.js": 22083, - "a.out.js.gz": 9166, + "a.out.js": 22157, + "a.out.js.gz": 9192, "a.out.nodebug.wasm": 1666, "a.out.nodebug.wasm.gz": 945, - "total": 23749, - "total_gz": 10111, + "total": 23823, + "total_gz": 10137, "sent": [ "a (fd_write)" ], diff --git a/test/codesize/test_codesize_files_js_fs.json b/test/codesize/test_codesize_files_js_fs.json index 02c02bac8d95a..97f72548b5a09 100644 --- a/test/codesize/test_codesize_files_js_fs.json +++ b/test/codesize/test_codesize_files_js_fs.json @@ -1,10 +1,10 @@ { - "a.out.js": 17829, - "a.out.js.gz": 7275, + "a.out.js": 17866, + "a.out.js.gz": 7289, "a.out.nodebug.wasm": 381, "a.out.nodebug.wasm.gz": 260, - "total": 18210, - "total_gz": 7535, + "total": 18247, + "total_gz": 7549, "sent": [ "a (fd_write)", "b (fd_read)", diff --git a/test/codesize/test_codesize_files_wasmfs.json b/test/codesize/test_codesize_files_wasmfs.json index 5095e9a9a3ffd..6ef587b23b45f 100644 --- a/test/codesize/test_codesize_files_wasmfs.json +++ b/test/codesize/test_codesize_files_wasmfs.json @@ -1,10 +1,10 @@ { - "a.out.js": 5353, - "a.out.js.gz": 2515, - "a.out.nodebug.wasm": 58324, - "a.out.nodebug.wasm.gz": 18215, - "total": 63677, - "total_gz": 20730, + "a.out.js": 5372, + "a.out.js.gz": 2527, + "a.out.nodebug.wasm": 58520, + "a.out.nodebug.wasm.gz": 18225, + "total": 63892, + "total_gz": 20752, "sent": [ "a (emscripten_date_now)", "b (emscripten_err)", diff --git a/test/codesize/test_codesize_hello_O0.json b/test/codesize/test_codesize_hello_O0.json index cda8320b130fe..35c3470b5e2e5 100644 --- a/test/codesize/test_codesize_hello_O0.json +++ b/test/codesize/test_codesize_hello_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 23572, - "a.out.js.gz": 8512, + "a.out.js": 23483, + "a.out.js.gz": 8523, "a.out.nodebug.wasm": 15115, "a.out.nodebug.wasm.gz": 7464, - "total": 38687, - "total_gz": 15976, + "total": 38598, + "total_gz": 15987, "sent": [ "fd_write" ], diff --git a/test/codesize/test_codesize_hello_O1.json b/test/codesize/test_codesize_hello_O1.json index bc88531f141c3..6fc1e13f37b50 100644 --- a/test/codesize/test_codesize_hello_O1.json +++ b/test/codesize/test_codesize_hello_O1.json @@ -1,10 +1,10 @@ { - "a.out.js": 6142, - "a.out.js.gz": 2375, + "a.out.js": 6081, + "a.out.js.gz": 2373, "a.out.nodebug.wasm": 2544, "a.out.nodebug.wasm.gz": 1436, - "total": 8686, - "total_gz": 3811, + "total": 8625, + "total_gz": 3809, "sent": [ "fd_write" ], diff --git a/test/codesize/test_codesize_hello_O2.json b/test/codesize/test_codesize_hello_O2.json index aee11df0b8919..0faee4aae5e9f 100644 --- a/test/codesize/test_codesize_hello_O2.json +++ b/test/codesize/test_codesize_hello_O2.json @@ -1,10 +1,10 @@ { - "a.out.js": 4204, - "a.out.js.gz": 2061, + "a.out.js": 4224, + "a.out.js.gz": 2073, "a.out.nodebug.wasm": 1912, "a.out.nodebug.wasm.gz": 1129, - "total": 6116, - "total_gz": 3190, + "total": 6136, + "total_gz": 3202, "sent": [ "fd_write" ], diff --git a/test/codesize/test_codesize_hello_O3.json b/test/codesize/test_codesize_hello_O3.json index 84e63cf7fd024..64fa4e9f093c2 100644 --- a/test/codesize/test_codesize_hello_O3.json +++ b/test/codesize/test_codesize_hello_O3.json @@ -1,10 +1,10 @@ { - "a.out.js": 4146, - "a.out.js.gz": 2018, + "a.out.js": 4166, + "a.out.js.gz": 2034, "a.out.nodebug.wasm": 1666, "a.out.nodebug.wasm.gz": 945, - "total": 5812, - "total_gz": 2963, + "total": 5832, + "total_gz": 2979, "sent": [ "a (fd_write)" ], diff --git a/test/codesize/test_codesize_hello_Os.json b/test/codesize/test_codesize_hello_Os.json index 0fdfbc8616e6f..08188848925a5 100644 --- a/test/codesize/test_codesize_hello_Os.json +++ b/test/codesize/test_codesize_hello_Os.json @@ -1,10 +1,10 @@ { - "a.out.js": 4146, - "a.out.js.gz": 2018, + "a.out.js": 4166, + "a.out.js.gz": 2034, "a.out.nodebug.wasm": 1654, "a.out.nodebug.wasm.gz": 953, - "total": 5800, - "total_gz": 2971, + "total": 5820, + "total_gz": 2987, "sent": [ "a (fd_write)" ], diff --git a/test/codesize/test_codesize_hello_Oz.json b/test/codesize/test_codesize_hello_Oz.json index e71b1037bac9a..0e864c65a4e75 100644 --- a/test/codesize/test_codesize_hello_Oz.json +++ b/test/codesize/test_codesize_hello_Oz.json @@ -1,10 +1,10 @@ { - "a.out.js": 3785, - "a.out.js.gz": 1825, + "a.out.js": 3801, + "a.out.js.gz": 1840, "a.out.nodebug.wasm": 1188, "a.out.nodebug.wasm.gz": 731, - "total": 4973, - "total_gz": 2556, + "total": 4989, + "total_gz": 2571, "sent": [ "a (fd_write)" ], diff --git a/test/codesize/test_codesize_hello_dylink.json b/test/codesize/test_codesize_hello_dylink.json index ee2792b0918a4..92ca5300b3a59 100644 --- a/test/codesize/test_codesize_hello_dylink.json +++ b/test/codesize/test_codesize_hello_dylink.json @@ -1,10 +1,10 @@ { - "a.out.js": 26254, - "a.out.js.gz": 11174, + "a.out.js": 26271, + "a.out.js.gz": 11190, "a.out.nodebug.wasm": 17861, "a.out.nodebug.wasm.gz": 9019, - "total": 44115, - "total_gz": 20193, + "total": 44132, + "total_gz": 20209, "sent": [ "__syscall_stat64", "emscripten_resize_heap", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index fa02be0975aab..380af5ac7a2db 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 268210, - "a.out.nodebug.wasm": 587552, - "total": 855762, + "a.out.js": 268255, + "a.out.nodebug.wasm": 587588, + "total": 855843, "sent": [ "IMG_Init", "IMG_Load", @@ -5324,9 +5324,11 @@ "$vwarnx", "$vwprintf", "$vwscanf", + "$wait", "$wait3", "$wait4", "$waitid", + "$waitpid", "$walk", "$warn", "$warnx", diff --git a/test/codesize/test_codesize_hello_export_nothing.json b/test/codesize/test_codesize_hello_export_nothing.json index e344c55a3ed16..37e4d982df25e 100644 --- a/test/codesize/test_codesize_hello_export_nothing.json +++ b/test/codesize/test_codesize_hello_export_nothing.json @@ -1,10 +1,10 @@ { - "a.out.js": 3061, - "a.out.js.gz": 1413, + "a.out.js": 3059, + "a.out.js.gz": 1422, "a.out.nodebug.wasm": 43, "a.out.nodebug.wasm.gz": 59, - "total": 3104, - "total_gz": 1472, + "total": 3102, + "total_gz": 1481, "sent": [], "imports": [], "exports": [ diff --git a/test/codesize/test_codesize_hello_single_file.json b/test/codesize/test_codesize_hello_single_file.json index cb7caf72bc49d..0e0d8db9e4571 100644 --- a/test/codesize/test_codesize_hello_single_file.json +++ b/test/codesize/test_codesize_hello_single_file.json @@ -1,6 +1,6 @@ { - "a.out.js": 5123, - "a.out.js.gz": 2813, + "a.out.js": 5141, + "a.out.js.gz": 2827, "sent": [ "a (fd_write)" ] diff --git a/test/codesize/test_codesize_hello_wasmfs.json b/test/codesize/test_codesize_hello_wasmfs.json index 84e63cf7fd024..64fa4e9f093c2 100644 --- a/test/codesize/test_codesize_hello_wasmfs.json +++ b/test/codesize/test_codesize_hello_wasmfs.json @@ -1,10 +1,10 @@ { - "a.out.js": 4146, - "a.out.js.gz": 2018, + "a.out.js": 4166, + "a.out.js.gz": 2034, "a.out.nodebug.wasm": 1666, "a.out.nodebug.wasm.gz": 945, - "total": 5812, - "total_gz": 2963, + "total": 5832, + "total_gz": 2979, "sent": [ "a (fd_write)" ], diff --git a/test/codesize/test_codesize_libcxxabi_message_O3.json b/test/codesize/test_codesize_libcxxabi_message_O3.json index de43289544a71..488aa8875ea60 100644 --- a/test/codesize/test_codesize_libcxxabi_message_O3.json +++ b/test/codesize/test_codesize_libcxxabi_message_O3.json @@ -1,10 +1,10 @@ { - "a.out.js": 3412, - "a.out.js.gz": 1594, + "a.out.js": 3428, + "a.out.js.gz": 1608, "a.out.nodebug.wasm": 89, "a.out.nodebug.wasm.gz": 98, - "total": 3501, - "total_gz": 1692, + "total": 3517, + "total_gz": 1706, "sent": [], "imports": [], "exports": [ diff --git a/test/codesize/test_codesize_libcxxabi_message_O3_standalone.json b/test/codesize/test_codesize_libcxxabi_message_O3_standalone.json index 7974b6c834cb0..a85247b0c82d5 100644 --- a/test/codesize/test_codesize_libcxxabi_message_O3_standalone.json +++ b/test/codesize/test_codesize_libcxxabi_message_O3_standalone.json @@ -1,10 +1,10 @@ { - "a.out.js": 3459, - "a.out.js.gz": 1628, + "a.out.js": 3475, + "a.out.js.gz": 1644, "a.out.nodebug.wasm": 222, "a.out.nodebug.wasm.gz": 206, - "total": 3681, - "total_gz": 1834, + "total": 3697, + "total_gz": 1850, "sent": [ "proc_exit" ], diff --git a/test/codesize/test_codesize_mem_O3.json b/test/codesize/test_codesize_mem_O3.json index 804bef1b21f61..651a91502de6c 100644 --- a/test/codesize/test_codesize_mem_O3.json +++ b/test/codesize/test_codesize_mem_O3.json @@ -1,10 +1,10 @@ { "a.out.js": 4241, - "a.out.js.gz": 2026, + "a.out.js.gz": 2031, "a.out.nodebug.wasm": 5260, "a.out.nodebug.wasm.gz": 2419, "total": 9501, - "total_gz": 4445, + "total_gz": 4450, "sent": [ "a (emscripten_resize_heap)" ], diff --git a/test/codesize/test_codesize_mem_O3_grow.json b/test/codesize/test_codesize_mem_O3_grow.json index 98fa9421f2b5a..57771a52cc8ab 100644 --- a/test/codesize/test_codesize_mem_O3_grow.json +++ b/test/codesize/test_codesize_mem_O3_grow.json @@ -1,10 +1,10 @@ { "a.out.js": 4526, - "a.out.js.gz": 2183, + "a.out.js.gz": 2185, "a.out.nodebug.wasm": 5261, "a.out.nodebug.wasm.gz": 2419, "total": 9787, - "total_gz": 4602, + "total_gz": 4604, "sent": [ "a (emscripten_resize_heap)" ], diff --git a/test/codesize/test_codesize_mem_O3_grow_standalone.json b/test/codesize/test_codesize_mem_O3_grow_standalone.json index 7f2e2f3f0cd23..3cb910c8e7f86 100644 --- a/test/codesize/test_codesize_mem_O3_grow_standalone.json +++ b/test/codesize/test_codesize_mem_O3_grow_standalone.json @@ -1,10 +1,10 @@ { - "a.out.js": 3997, - "a.out.js.gz": 1923, + "a.out.js": 3995, + "a.out.js.gz": 1927, "a.out.nodebug.wasm": 5641, "a.out.nodebug.wasm.gz": 2659, - "total": 9638, - "total_gz": 4582, + "total": 9636, + "total_gz": 4586, "sent": [ "args_get", "args_sizes_get", diff --git a/test/codesize/test_codesize_mem_O3_standalone.json b/test/codesize/test_codesize_mem_O3_standalone.json index 38903d82e83de..41ea30643af6c 100644 --- a/test/codesize/test_codesize_mem_O3_standalone.json +++ b/test/codesize/test_codesize_mem_O3_standalone.json @@ -1,10 +1,10 @@ { - "a.out.js": 3930, - "a.out.js.gz": 1883, + "a.out.js": 3928, + "a.out.js.gz": 1890, "a.out.nodebug.wasm": 5565, "a.out.nodebug.wasm.gz": 2598, - "total": 9495, - "total_gz": 4481, + "total": 9493, + "total_gz": 4488, "sent": [ "args_get", "args_sizes_get", diff --git a/test/codesize/test_codesize_mem_O3_standalone_lib.json b/test/codesize/test_codesize_mem_O3_standalone_lib.json index 8ea668d1c9cc8..8cfbd2cf6a1a0 100644 --- a/test/codesize/test_codesize_mem_O3_standalone_lib.json +++ b/test/codesize/test_codesize_mem_O3_standalone_lib.json @@ -1,10 +1,10 @@ { - "a.out.js": 3452, - "a.out.js.gz": 1622, + "a.out.js": 3468, + "a.out.js.gz": 1635, "a.out.nodebug.wasm": 5239, "a.out.nodebug.wasm.gz": 2359, - "total": 8691, - "total_gz": 3981, + "total": 8707, + "total_gz": 3994, "sent": [], "imports": [], "exports": [ diff --git a/test/codesize/test_codesize_mem_O3_standalone_narg.json b/test/codesize/test_codesize_mem_O3_standalone_narg.json index 708ea064e8a48..6189af6c29091 100644 --- a/test/codesize/test_codesize_mem_O3_standalone_narg.json +++ b/test/codesize/test_codesize_mem_O3_standalone_narg.json @@ -1,10 +1,10 @@ { - "a.out.js": 3459, - "a.out.js.gz": 1628, + "a.out.js": 3475, + "a.out.js.gz": 1644, "a.out.nodebug.wasm": 5354, "a.out.nodebug.wasm.gz": 2442, - "total": 8813, - "total_gz": 4070, + "total": 8829, + "total_gz": 4086, "sent": [ "proc_exit" ], diff --git a/test/codesize/test_codesize_mem_O3_standalone_narg_flto.json b/test/codesize/test_codesize_mem_O3_standalone_narg_flto.json index ded88f07b1e2b..e654914a333d4 100644 --- a/test/codesize/test_codesize_mem_O3_standalone_narg_flto.json +++ b/test/codesize/test_codesize_mem_O3_standalone_narg_flto.json @@ -1,10 +1,10 @@ { - "a.out.js": 3459, - "a.out.js.gz": 1628, + "a.out.js": 3475, + "a.out.js.gz": 1644, "a.out.nodebug.wasm": 4285, "a.out.nodebug.wasm.gz": 2142, - "total": 7744, - "total_gz": 3770, + "total": 7760, + "total_gz": 3786, "sent": [ "proc_exit" ], diff --git a/test/codesize/test_codesize_minimal_O0.expected.js b/test/codesize/test_codesize_minimal_O0.expected.js index 5e6fbe3e6b888..b97d7b35a7ad0 100644 --- a/test/codesize/test_codesize_minimal_O0.expected.js +++ b/test/codesize/test_codesize_minimal_O0.expected.js @@ -1338,29 +1338,21 @@ function stackCheckInit() { } function run() { + assert(!calledRun); + calledRun = true; stackCheckInit(); preRun(); - function doRun() { - // run may have just been called through dependencies being fulfilled just in this very frame, - // or while the async setStatus time below was happening - assert(!calledRun); - calledRun = true; - - if (ABORT) return; + if (ABORT) return; - initRuntime(); + initRuntime(); - assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'); + assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'); - postRun(); - } + postRun(); - { - doRun(); - } checkStackCookie(); } diff --git a/test/codesize/test_codesize_minimal_O0.json b/test/codesize/test_codesize_minimal_O0.json index e27d8b48cbc3d..325e0d534493a 100644 --- a/test/codesize/test_codesize_minimal_O0.json +++ b/test/codesize/test_codesize_minimal_O0.json @@ -1,9 +1,9 @@ { - "a.out.js": 18725, + "a.out.js": 18723, "a.out.js.gz": 6778, "a.out.nodebug.wasm": 1015, "a.out.nodebug.wasm.gz": 602, - "total": 19740, + "total": 19738, "total_gz": 7380, "sent": [], "imports": [], diff --git a/test/codesize/test_small_js_flags.expected.js b/test/codesize/test_small_js_flags.expected.js index 7cf5eeccec298..798ff6cc0266a 100644 --- a/test/codesize/test_small_js_flags.expected.js +++ b/test/codesize/test_small_js_flags.expected.js @@ -441,19 +441,12 @@ function callMain() { function run() { preRun(); - function doRun() { - // run may have just been called through dependencies being fulfilled just in this very frame, - // or while the async setStatus time below was happening - if (ABORT) return; - initRuntime(); - preMain(); - var noInitialRun = false; - if (!noInitialRun) callMain(); - postRun(); - } - { - doRun(); - } + if (ABORT) return; + initRuntime(); + preMain(); + var noInitialRun = false; + if (!noInitialRun) callMain(); + postRun(); } var wasmExports; diff --git a/test/codesize/test_unoptimized_code_size.json b/test/codesize/test_unoptimized_code_size.json index 214e66be63154..3e8dbb92eef88 100644 --- a/test/codesize/test_unoptimized_code_size.json +++ b/test/codesize/test_unoptimized_code_size.json @@ -1,16 +1,16 @@ { - "hello_world.js": 55499, - "hello_world.js.gz": 17473, + "hello_world.js": 55305, + "hello_world.js.gz": 17405, "hello_world.wasm": 15115, "hello_world.wasm.gz": 7464, - "no_asserts.js": 25871, - "no_asserts.js.gz": 8759, + "no_asserts.js": 25683, + "no_asserts.js.gz": 8690, "no_asserts.wasm": 12229, "no_asserts.wasm.gz": 6004, - "strict.js": 53256, - "strict.js.gz": 16714, + "strict.js": 53033, + "strict.js.gz": 16620, "strict.wasm": 15115, "strict.wasm.gz": 7461, - "total": 177085, - "total_gz": 63875 + "total": 176480, + "total_gz": 63644 }