From 98fc66bb1b4e21118d0af0afd8131979a4d1c17f Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Thu, 29 Jul 2021 16:54:51 -0400 Subject: [PATCH] Add support for enabling WASM code paths in NodeJS package See `test.js` for reference on how to enable WASM code paths (which are disabled by default). --- platform/common/vapi-background.js | 2 +- platform/nodejs/main.js | 34 +++++++++-- platform/nodejs/test.js | 14 +++-- src/js/biditrie.js | 20 ++----- src/js/hntrie.js | 21 ++----- src/js/start.js | 11 +++- src/js/static-net-filtering.js | 12 ++-- src/js/storage.js | 31 ++++++++-- src/lib/publicsuffixlist/publicsuffixlist.js | 62 +++++++++----------- tools/make-nodejs.sh | 3 + 10 files changed, 123 insertions(+), 87 deletions(-) diff --git a/platform/common/vapi-background.js b/platform/common/vapi-background.js index b9f7e2721..f1c63c5b8 100644 --- a/platform/common/vapi-background.js +++ b/platform/common/vapi-background.js @@ -36,7 +36,7 @@ vAPI.cantWebsocket = vAPI.canWASM = vAPI.webextFlavor.soup.has('chromium') === false; if ( vAPI.canWASM === false ) { const csp = manifest.content_security_policy; - vAPI.canWASM = csp !== undefined && csp.indexOf("'wasm-eval'") !== -1; + vAPI.canWASM = csp !== undefined && csp.indexOf("'unsafe-eval'") !== -1; } vAPI.supportsUserStylesheets = vAPI.webextFlavor.soup.has('user_stylesheet'); diff --git a/platform/nodejs/main.js b/platform/nodejs/main.js index 6874bdf94..953c73cb4 100644 --- a/platform/nodejs/main.js +++ b/platform/nodejs/main.js @@ -80,11 +80,35 @@ function applyList(name, raw) { snfe.fromCompiled(reader); } -function enableWASM(path) { - return Promise.all([ - globals.publicSuffixList.enableWASM(`${path}/lib/publicsuffixlist`), - snfe.enableWASM(`${path}/js`), - ]); +async function enableWASM() { + const wasmModuleFetcher = async function(path) { + return new Promise(async (resolve, reject) => { + const require = createRequire(import.meta.url); // jshint ignore:line + const fs = require('fs'); + fs.readFile(`${path}.wasm`, null, (err, data) => { + if ( err ) { return reject(err); } + return globals.WebAssembly.compile(data).then(module => { + resolve(module); + }); + }); + }); + }; + try { + const results = await Promise.all([ + globals.publicSuffixList.enableWASM( + wasmModuleFetcher, + './lib/publicsuffixlist/wasm/' + ), + snfe.enableWASM( + wasmModuleFetcher, + './js/wasm/' + ), + ]); + return results.every(a => a === true); + } catch(reason) { + console.info(reason); + } + return false; } function pslInit(raw) { diff --git a/platform/nodejs/test.js b/platform/nodejs/test.js index c4735e1bc..f9cf9849d 100644 --- a/platform/nodejs/test.js +++ b/platform/nodejs/test.js @@ -28,6 +28,7 @@ import { createRequire } from 'module'; import { + enableWASM, FilteringContext, pslInit, restart, @@ -43,13 +44,14 @@ function fetch(listName) { } (async ( ) => { - /* - * WASM require fetch(), not present in Node - try { - await enableWASM('//ublock/dist/build/uBlock0.nodejs'); - } catch(ex) { + try { + const result = await enableWASM(); + if ( result !== true ) { + console.log('Failed to enable all WASM code paths'); } - */ + } catch(ex) { + console.log(ex); + } await pslInit(); diff --git a/src/js/biditrie.js b/src/js/biditrie.js index 78dc7754c..269ac1b88 100644 --- a/src/js/biditrie.js +++ b/src/js/biditrie.js @@ -692,10 +692,10 @@ const BidiTrieContainer = class { return -1; } - async enableWASM(modulePath) { + async enableWASM(wasmModuleFetcher, path) { if ( typeof WebAssembly !== 'object' ) { return false; } if ( this.wasmMemory instanceof WebAssembly.Memory ) { return true; } - const module = await getWasmModule(modulePath); + const module = await getWasmModule(wasmModuleFetcher, path); if ( module instanceof WebAssembly.Module === false ) { return false; } const memory = new WebAssembly.Memory({ initial: roundToPageSize(this.buf8.length) >>> 16 @@ -925,17 +925,12 @@ BidiTrieContainer.prototype.STrieRef = class { const getWasmModule = (( ) => { let wasmModulePromise; - return async function(modulePath) { + return async function(wasmModuleFetcher, path) { if ( wasmModulePromise instanceof Promise ) { return wasmModulePromise; } - if ( - typeof WebAssembly !== 'object' || - typeof WebAssembly.compileStreaming !== 'function' - ) { - return; - } + if ( typeof WebAssembly !== 'object' ) { return; } // Soft-dependency on vAPI so that the code here can be used outside of // uBO (i.e. tests, benchmarks) @@ -948,12 +943,7 @@ const getWasmModule = (( ) => { uint32s[0] = 1; if ( uint8s[0] !== 1 ) { return; } - wasmModulePromise = fetch( - `${modulePath}/wasm/biditrie.wasm`, - { mode: 'same-origin' } - ).then( - WebAssembly.compileStreaming - ).catch(reason => { + wasmModulePromise = wasmModuleFetcher(`${path}biditrie`).catch(reason => { console.info(reason); }); diff --git a/src/js/hntrie.js b/src/js/hntrie.js index abebf2d65..3fb172556 100644 --- a/src/js/hntrie.js +++ b/src/js/hntrie.js @@ -456,10 +456,10 @@ const HNTrieContainer = class { return n === hr || hn.charCodeAt(hl-1) === 0x2E /* '.' */; } - async enableWASM(modulePath) { + async enableWASM(wasmModuleFetcher, path) { if ( typeof WebAssembly !== 'object' ) { return false; } if ( this.wasmMemory instanceof WebAssembly.Memory ) { return true; } - const module = await getWasmModule(modulePath); + const module = await getWasmModule(wasmModuleFetcher, path); if ( module instanceof WebAssembly.Module === false ) { return false; } const memory = new WebAssembly.Memory({ initial: 2 }); const instance = await WebAssembly.instantiate(module, { @@ -483,6 +483,7 @@ const HNTrieContainer = class { this.buf32 = new Uint32Array(this.buf.buffer); this.matches = this.matchesWASM = instance.exports.matches; this.add = this.addWASM = instance.exports.add; + return true; } //-------------------------------------------------------------------------- @@ -767,17 +768,12 @@ HNTrieContainer.prototype.HNTrieRef.prototype.needle = ''; const getWasmModule = (( ) => { let wasmModulePromise; - return async function(modulePath) { + return async function(wasmModuleFetcher, path) { if ( wasmModulePromise instanceof Promise ) { return wasmModulePromise; } - if ( - typeof WebAssembly !== 'object' || - typeof WebAssembly.compileStreaming !== 'function' - ) { - return; - } + if ( typeof WebAssembly !== 'object' ) { return; } // Soft-dependency on vAPI so that the code here can be used outside of // uBO (i.e. tests, benchmarks) @@ -790,12 +786,7 @@ const getWasmModule = (( ) => { uint32s[0] = 1; if ( uint8s[0] !== 1 ) { return; } - wasmModulePromise = fetch( - `${modulePath}/wasm/hntrie.wasm`, - { mode: 'same-origin' } - ).then( - WebAssembly.compileStreaming - ).catch(reason => { + wasmModulePromise = wasmModuleFetcher(`${path}hntrie`).catch(reason => { console.info(reason); }); diff --git a/src/js/start.js b/src/js/start.js index e3514f7c8..d50b75c2a 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -25,6 +25,7 @@ import cacheStorage from './cachestorage.js'; import contextMenu from './contextmenu.js'; +import globals from './globals.js'; import io from './assets.js'; import lz4Codec from './lz4.js'; import staticExtFilteringEngine from './static-ext-filtering.js'; @@ -343,7 +344,15 @@ try { } if ( µb.hiddenSettings.disableWebAssembly !== true ) { - staticNetFilteringEngine.enableWASM('/js').then(( ) => { + const wasmModuleFetcher = async function(path) { + return fetch(`${path}.wasm`, { mode: 'same-origin' }).then( + globals.WebAssembly.compileStreaming + ).catch(reason => { + ubolog(reason); + }); + }; + staticNetFilteringEngine.enableWASM(wasmModuleFetcher, './js/wasm/').then(result => { + if ( result !== true ) { return; } ubolog(`WASM modules ready ${Date.now()-vAPI.T0} ms after launch`); }); } diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js index d0eb6aaf2..25fe86450 100644 --- a/src/js/static-net-filtering.js +++ b/src/js/static-net-filtering.js @@ -4448,12 +4448,14 @@ FilterContainer.prototype.getFilterCount = function() { /******************************************************************************/ -FilterContainer.prototype.enableWASM = function(modulePath) { +FilterContainer.prototype.enableWASM = function(wasmModuleFetcher, path) { return Promise.all([ - bidiTrie.enableWASM(modulePath), - filterOrigin.trieContainer.enableWASM(modulePath), - FilterHostnameDict.trieContainer.enableWASM(modulePath), - ]); + bidiTrie.enableWASM(wasmModuleFetcher, path), + filterOrigin.trieContainer.enableWASM(wasmModuleFetcher, path), + FilterHostnameDict.trieContainer.enableWASM(wasmModuleFetcher, path), + ]).then(results => { + return results.every(a => a === true); + }); }; /******************************************************************************/ diff --git a/src/js/storage.js b/src/js/storage.js index 0bf1b392d..76c1f3e93 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -1194,17 +1194,36 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { µb.loadPublicSuffixList = async function() { const psl = globals.publicSuffixList; + + // WASM is nice but not critical if ( this.hiddenSettings.disableWebAssembly !== true ) { - psl.enableWASM('/lib/publicsuffixlist'); + const wasmModuleFetcher = function(path) { + return fetch( `${path}.wasm`, { + mode: 'same-origin' + }).then( + globals.WebAssembly.compileStreaming + ).catch(reason => { + ubolog(reason); + }); + }; + let result = false; + try { + result = await psl.enableWASM(wasmModuleFetcher, + './lib/publicsuffixlist/wasm/' + ); + } catch(reason) { + ubolog(reason); + } + if ( result ) { + ubolog(`WASM PSL ready ${Date.now()-vAPI.T0} ms after launch`); + } } try { const result = await io.get(`compiled/${this.pslAssetKey}`); - if ( psl.fromSelfie(result.content, sparseBase64) ) { - return; - } - } catch (ex) { - ubolog(ex); + if ( psl.fromSelfie(result.content, sparseBase64) ) { return; } + } catch (reason) { + ubolog(reason); } const result = await io.get(this.pslAssetKey); diff --git a/src/lib/publicsuffixlist/publicsuffixlist.js b/src/lib/publicsuffixlist/publicsuffixlist.js index a073d49ac..1ffdc35f5 100644 --- a/src/lib/publicsuffixlist/publicsuffixlist.js +++ b/src/lib/publicsuffixlist/publicsuffixlist.js @@ -13,8 +13,7 @@ /*! Home: https://github.com/gorhill/publicsuffixlist.js -- GPLv3 APLv2 */ -/* jshint browser:true, esversion:6, laxbreak:true, undef:true, unused:true */ -/* globals WebAssembly, console, exports:true, module */ +/* globals WebAssembly, exports:true, module */ 'use strict'; @@ -528,43 +527,33 @@ const fromSelfie = function(selfie, decoder) { // The WASM module is entirely optional, the JS implementation will be // used should the WASM module be unavailable for whatever reason. -const enableWASM = (function() { - let memory; - - return function(modulePath) { - if ( getPublicSuffixPosWASM instanceof Function ) { - return Promise.resolve(true); - } - - if ( - typeof WebAssembly !== 'object' || - typeof WebAssembly.instantiateStreaming !== 'function' - ) { - return Promise.resolve(false); - } +const enableWASM = (( ) => { + let wasmPromise; + const getWasmInstance = async function(wasmModuleFetcher, path) { + if ( typeof WebAssembly !== 'object' ) { return false; } // The wasm code will work only if CPU is natively little-endian, // as we use native uint32 array in our js code. const uint32s = new Uint32Array(1); const uint8s = new Uint8Array(uint32s.buffer); uint32s[0] = 1; - if ( uint8s[0] !== 1 ) { - return Promise.resolve(false); - } + if ( uint8s[0] !== 1 ) { return false; } - return fetch( - `${modulePath}/wasm/publicsuffixlist.wasm`, - { mode: 'same-origin' } - ).then(response => { + try { + const module = await wasmModuleFetcher(`${path}publicsuffixlist`); + if ( module instanceof WebAssembly.Module === false ) { + return false; + } const pageCount = pslBuffer8 !== undefined ? pslBuffer8.byteLength + 0xFFFF >>> 16 : 1; - memory = new WebAssembly.Memory({ initial: pageCount }); - return WebAssembly.instantiateStreaming( - response, - { imports: { memory: memory } } - ); - }).then(({ instance }) => { + const memory = new WebAssembly.Memory({ initial: pageCount }); + const instance = await WebAssembly.instantiate(module, { + imports: { memory } + }); + if ( instance instanceof WebAssembly.Instance === false ) { + return false; + } const curPageCount = memory.buffer.byteLength >>> 16; const newPageCount = pslBuffer8 !== undefined ? pslBuffer8.byteLength + 0xFFFF >>> 16 @@ -582,12 +571,19 @@ const enableWASM = (function() { wasmMemory = memory; getPublicSuffixPosWASM = instance.exports.getPublicSuffixPos; getPublicSuffixPos = getPublicSuffixPosWASM; - memory = undefined; return true; - }).catch(reason => { + } catch(reason) { console.info(reason); - return false; - }); + } + return false; + }; + + return async function(wasmModuleFetcher, path) { + if ( getPublicSuffixPosWASM instanceof Function ) { return true; } + if ( wasmPromise instanceof Promise === false ) { + wasmPromise = getWasmInstance(wasmModuleFetcher, path); + } + return wasmPromise; }; })(); diff --git a/tools/make-nodejs.sh b/tools/make-nodejs.sh index 186731650..2fe664d5a 100755 --- a/tools/make-nodejs.sh +++ b/tools/make-nodejs.sh @@ -16,6 +16,9 @@ cp src/js/static-filtering-io.js $DES/js cp src/js/text-iterators.js $DES/js cp src/js/uri-utils.js $DES/js +mkdir -p $DES/js/wasm +cp src/js/wasm/* $DES/js/wasm/ + mkdir -p $DES/lib cp -R src/lib/punycode.js $DES/lib/ cp -R src/lib/publicsuffixlist $DES/lib/