| .. _CrossOriginStorage: |
| |
| ========================== |
| Cross-Origin Storage (COS) |
| ========================== |
| |
| .. note:: |
| This feature is **experimental**. The underlying `Cross-Origin Storage |
| browser API <https://github.com/WICG/cross-origin-storage>`_ 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 :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 |
| 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. |
| |
| When to use this flag |
| --------------------- |
| |
| COS only delivers a benefit when the ``.wasm`` binary is **byte-identical |
| 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: |
| |
| - 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). |
| |
| **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. |
| |
| 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 :ref:`CROSS_ORIGIN_STORAGE_ORIGINS` to |
| restrict access to only those origins rather than opening the cache entry to |
| the world. |
| |
| Usage |
| ===== |
| |
| Pass :ref:`CROSS_ORIGIN_STORAGE` at link time:: |
| |
| emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE |
| |
| Controlling which origins can read the cached file |
| -------------------------------------------------- |
| |
| The :ref:`CROSS_ORIGIN_STORAGE_ORIGINS` setting controls the ``origins`` field |
| passed to ``requestFileHandle()`` 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 |
| :ref:`CROSS_ORIGIN_STORAGE` is used without specifying |
| :ref:`CROSS_ORIGIN_STORAGE_ORIGINS`: |
| |
| .. code-block:: bash |
| |
| 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. |
| |
| **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 \ |
| -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 + |
| optional port, no path). Mixing ``'*'`` with explicit origins is a |
| **link-time error**. |
| |
| **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 |
| |
| emcc hello.cpp -o hello.js \ |
| -sCROSS_ORIGIN_STORAGE \ |
| -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. |
| |
| 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 |
| ------------------------------ |
| |
| - 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 |
| hash on. |
| - 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 |
| 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. |
| |
| How it works |
| ============ |
| |
| Build time |
| ---------- |
| |
| After all optimizations — including any ``wasm-opt`` passes run by Binaryen |
| — 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' }; |
| |
| 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 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 |
| |
| # After all post-processing is complete: |
| final_hash=$(sha256sum hello.wasm | awk '{print $1}') |
| 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 |
| sed syntax. |
| |
| 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.requestFileHandle(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()``. |
| 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'](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 |
| :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, |
| 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. |
| |
| Instrumentation callbacks |
| ------------------------- |
| |
| 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 |
| |
| 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. |hash| is the hash 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. |
| onCOSStore: (hash) => { |
| console.log('Stored in COS, SHA-256:', hash); |
| }, |
| }; |
| |
| Testing with the extension polyfill |
| ==================================== |
| |
| Because no browser ships the COS API natively yet, you can experiment using |
| the `Cross-Origin Storage extension |
| <https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih>`_, |
| 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``). |
| 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. |
| |
| 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 |
| ============================ |
| |
| 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``. |
| |
| 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['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['wasmHash']`` |
| 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 = Module['wasmHash']; |
| if (cosHash?.value && globalThis.navigator?.crossOriginStorage) { |
| navigator.crossOriginStorage.requestFileHandles(cosHash) |
| .then(handle => handle.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 |
| .requestFileHandle(cosHash, { create: true, origins: '*' }) |
| .then(wh => wh.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['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. |
| |
| 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 <https://github.com/WICG/cross-origin-storage>`_ |
| - `COS browser extension (Chrome Web Store) <https://chromewebstore.google.com/detail/cross-origin-storage/denpnpcgjgikjpoglpjefakmdcbmlgih>`_ |
| - `COS browser extension (source code) <https://github.com/web-ai-community/cross-origin-storage-extension>`_ |
| - :ref:`settings-reference` — ``CROSS_ORIGIN_STORAGE`` entry |
| - :ref:`WebAssembly` — general guide to building Wasm with Emscripten |