├── .gitignore ├── wasm_mod ├── .gitignore ├── tests │ └── web.rs ├── src │ ├── utils.rs │ └── lib.rs ├── build.bat └── Cargo.toml ├── extension ├── js │ ├── wasm │ │ ├── wasm_mod_bg.wasm │ │ └── wasm_mod.js │ ├── background.js │ └── content.js └── manifest.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /wasm_mod/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /target 3 | **/*.rs.bk 4 | Cargo.lock 5 | bin/ 6 | pkg/ 7 | wasm-pack.log 8 | -------------------------------------------------------------------------------- /extension/js/wasm/wasm_mod_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udoless/rust-wasm-chrome-ext/master/extension/js/wasm/wasm_mod_bg.wasm -------------------------------------------------------------------------------- /extension/js/background.js: -------------------------------------------------------------------------------- 1 | // Use static import 2 | import initWasmModule, { hello_background } from './wasm/wasm_mod.js'; 3 | 4 | 5 | (async () => { 6 | await initWasmModule(); 7 | hello_background(); 8 | })(); -------------------------------------------------------------------------------- /wasm_mod/tests/web.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for the Web and headless browsers. 2 | 3 | #![cfg(target_arch = "wasm32")] 4 | 5 | extern crate wasm_bindgen_test; 6 | use wasm_bindgen_test::*; 7 | 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | 10 | #[wasm_bindgen_test] 11 | fn pass () { 12 | assert_eq!(1 + 1, 2); 13 | } 14 | -------------------------------------------------------------------------------- /wasm_mod/src/utils.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | pub fn set_panic_hook () { 3 | // When the `console_error_panic_hook` feature is enabled, we can call the 4 | // `set_panic_hook` function at least once during initialization, and then 5 | // we will get better error messages if our code ever panics. 6 | // 7 | // For more details see 8 | // https://github.com/rustwasm/console_error_panic_hook#readme 9 | #[cfg(feature = "console_error_panic_hook")] 10 | console_error_panic_hook::set_once(); 11 | } 12 | -------------------------------------------------------------------------------- /wasm_mod/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use wasm_bindgen::prelude::*; 4 | 5 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 6 | // allocator. 7 | #[cfg(feature="wee_alloc")] 8 | #[global_allocator] 9 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 10 | 11 | #[wasm_bindgen] 12 | extern { 13 | fn alert (s: &str); 14 | 15 | #[wasm_bindgen(js_namespace=console)] 16 | fn log (s: &str); 17 | } 18 | 19 | // Will be called in content.js 20 | #[wasm_bindgen] 21 | pub fn hello_content () { 22 | alert("Hello from the content script!"); 23 | } 24 | 25 | // Will be called in background.js 26 | #[wasm_bindgen] 27 | pub fn hello_background () { 28 | log("Hello from the background script!"); 29 | } 30 | -------------------------------------------------------------------------------- /wasm_mod/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: --release - use release profile build 4 | :: --no-typescript - disable .d.ts files output 5 | :: --out-dir - set output directory 6 | :: --out-name - force output file names 7 | :: --target - always use "web"! 8 | :: See https://rustwasm.github.io/wasm-pack/book/commands/build.html 9 | echo Building wasm module... 10 | wasm-pack build --release --no-typescript --out-dir "../extension/js/wasm" --out-name "wasm_mod" --target web 11 | 12 | :: wasm-pack creates bunch of useless files: 13 | :: - Output of typescript files disabled by --no-typescript wasm-pack argument 14 | :: - We should delete the .gitignore and package.json files ourselves 15 | echo Removing trash files... 16 | if exist "..\extension\js\wasm\.gitignore" del "..\extension\js\wasm\.gitignore" 17 | if exist "..\extension\js\wasm\package.json" del "..\extension\js\wasm\package.json" 18 | 19 | echo Done 20 | pause -------------------------------------------------------------------------------- /extension/js/content.js: -------------------------------------------------------------------------------- 1 | const WASM_MOD_URL = chrome.runtime.getURL('js/wasm/wasm_mod.js'); 2 | 3 | 4 | // Import Wasm module binding using dynamic import. 5 | // "init" may fail if the current site CSP restricts the use of Wasm (e.g. any github.com page). 6 | // In this case instantiate module in the background worker (see background.js) and use message passing. 7 | const loadWasmModule = async () => { 8 | const mod = await import(WASM_MOD_URL); 9 | 10 | // default export is an init function 11 | const isOk = await mod.default().catch((e) => { 12 | console.warn('Failed to init wasm module in content script. Probably CSP of the page has restricted wasm loading.', e); 13 | return null; 14 | }); 15 | 16 | return isOk ? mod : null; 17 | }; 18 | 19 | 20 | (async () => { 21 | const mod = await loadWasmModule(); 22 | 23 | // If the module is successfully initialized, 24 | // import entities from the module 25 | if (mod) { 26 | const { hello_content } = mod; 27 | 28 | hello_content(); 29 | } 30 | })(); -------------------------------------------------------------------------------- /wasm_mod/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm_mod" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = [ "Stanislav Berrigan" ] 6 | homepage = "https://github.com/theberrigan/rust-wasm-chrome-ext" 7 | repository = "https://github.com/theberrigan/rust-wasm-chrome-ext" 8 | 9 | [lib] 10 | crate-type = [ 11 | "cdylib", 12 | "rlib" 13 | ] 14 | 15 | [features] 16 | default = [ 17 | "console_error_panic_hook" 18 | ] 19 | 20 | [dependencies] 21 | wasm-bindgen = "0.2.86" 22 | 23 | # The `console_error_panic_hook` crate provides better debugging of panics by 24 | # logging them with `console.error`. This is great for development, but requires 25 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 26 | # code size when deploying. 27 | console_error_panic_hook = { version = "0.1.7", optional = true } 28 | 29 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 30 | # compared to the default allocator's ~10K. It is slower than the default 31 | # allocator, however. 32 | wee_alloc = { version = "0.4.5", optional = true } 33 | 34 | [dev-dependencies] 35 | wasm-bindgen-test = "0.3.36" 36 | 37 | [profile.release] 38 | # Tell `rustc` to optimize for small code size. 39 | opt-level = 3 40 | -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Chrome Wasm Extension", 4 | "short_name": "Chrome Wasm Extension", 5 | "description": "", 6 | "version": "1.0.0", 7 | "author": "Berrigan", 8 | "minimum_chrome_version": "110", 9 | "offline_enabled": true, 10 | "action": {}, 11 | "content_security_policy": { 12 | "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';" 13 | }, 14 | "background": { 15 | "type": "module", 16 | "service_worker": "js/background.js" 17 | }, 18 | "permissions": [ 19 | "tabs", 20 | "alarms", 21 | "scripting" 22 | ], 23 | "host_permissions": [ 24 | "*://*/*", 25 | "file:///*/*" 26 | ], 27 | "content_scripts": [ 28 | { 29 | "run_at": "document_end", 30 | "all_frames": false, 31 | "matches": [ 32 | "*://*/*" 33 | ], 34 | "js": [ 35 | "js/content.js" 36 | ] 37 | } 38 | ], 39 | "web_accessible_resources": [ 40 | { 41 | "matches": [ 42 | "" 43 | ], 44 | "resources": [ 45 | "js/wasm/wasm_mod.js", 46 | "js/wasm/wasm_mod_bg.wasm" 47 | ] 48 | } 49 | ] 50 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Web Assembly Chrome Extension Example 2 | 3 | ## Setup 4 | 1. Install [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/): 5 | 6 | ``` 7 | cargo install wasm-pack 8 | ``` 9 | 2. Go to ```/wasm_mod``` and run ```build.bat```.
10 | It will compile the ```.wasm``` module and ```.js```-wrapper for it and put them in the ```extension/js/wasm``` 11 | 3. Go to Chrome extensions page and load unpacked extension from ```/extension``` 12 | 13 | ## Notes 14 | - ```extension/js/content.js``` demonstrates how to load wasm into the content script
15 | **Important:** in the content script, the module can only be loaded for those sites whose Content Security Policy does not prohibit it 16 | - ```extension/js/background.js``` demonstrates how to load wasm into the background worker script 17 | - For ```wasm-pack``` always use ```--target web``` 18 | - ```manifest.json```: 19 | - To load the wasm module into the content script, you should list ```.wasm``` and corresponding ```.js``` in the ```web_accessible_resources.resources``` section of manifest 20 | - To load the wasm module into the background worker script, you should specify ```wasm-unsafe-eval``` in the ```content_security_policy.extension_pages``` section of manifest 21 | 22 | ## Tested with 23 | - Chrome 114 (extension manifest v3) 24 | - Rust 1.70 (edition 2021) 25 | - wasm-bindgen 0.2.86 26 | - wasm-pack 0.11.1 -------------------------------------------------------------------------------- /extension/js/wasm/wasm_mod.js: -------------------------------------------------------------------------------- 1 | let wasm; 2 | 3 | const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); 4 | 5 | if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; 6 | 7 | let cachedUint8Memory0 = null; 8 | 9 | function getUint8Memory0() { 10 | if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) { 11 | cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); 12 | } 13 | return cachedUint8Memory0; 14 | } 15 | 16 | function getStringFromWasm0(ptr, len) { 17 | ptr = ptr >>> 0; 18 | return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); 19 | } 20 | /** 21 | */ 22 | export function hello_content() { 23 | wasm.hello_content(); 24 | } 25 | 26 | /** 27 | */ 28 | export function hello_background() { 29 | wasm.hello_background(); 30 | } 31 | 32 | async function __wbg_load(module, imports) { 33 | if (typeof Response === 'function' && module instanceof Response) { 34 | if (typeof WebAssembly.instantiateStreaming === 'function') { 35 | try { 36 | return await WebAssembly.instantiateStreaming(module, imports); 37 | 38 | } catch (e) { 39 | if (module.headers.get('Content-Type') != 'application/wasm') { 40 | console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); 41 | 42 | } else { 43 | throw e; 44 | } 45 | } 46 | } 47 | 48 | const bytes = await module.arrayBuffer(); 49 | return await WebAssembly.instantiate(bytes, imports); 50 | 51 | } else { 52 | const instance = await WebAssembly.instantiate(module, imports); 53 | 54 | if (instance instanceof WebAssembly.Instance) { 55 | return { instance, module }; 56 | 57 | } else { 58 | return instance; 59 | } 60 | } 61 | } 62 | 63 | function __wbg_get_imports() { 64 | const imports = {}; 65 | imports.wbg = {}; 66 | imports.wbg.__wbg_alert_2b986da32dea251d = function(arg0, arg1) { 67 | alert(getStringFromWasm0(arg0, arg1)); 68 | }; 69 | imports.wbg.__wbg_log_3167b2eeae350bd7 = function(arg0, arg1) { 70 | console.log(getStringFromWasm0(arg0, arg1)); 71 | }; 72 | 73 | return imports; 74 | } 75 | 76 | function __wbg_init_memory(imports, maybe_memory) { 77 | 78 | } 79 | 80 | function __wbg_finalize_init(instance, module) { 81 | wasm = instance.exports; 82 | __wbg_init.__wbindgen_wasm_module = module; 83 | cachedUint8Memory0 = null; 84 | 85 | 86 | return wasm; 87 | } 88 | 89 | function initSync(module) { 90 | if (wasm !== undefined) return wasm; 91 | 92 | const imports = __wbg_get_imports(); 93 | 94 | __wbg_init_memory(imports); 95 | 96 | if (!(module instanceof WebAssembly.Module)) { 97 | module = new WebAssembly.Module(module); 98 | } 99 | 100 | const instance = new WebAssembly.Instance(module, imports); 101 | 102 | return __wbg_finalize_init(instance, module); 103 | } 104 | 105 | async function __wbg_init(input) { 106 | if (wasm !== undefined) return wasm; 107 | 108 | if (typeof input === 'undefined') { 109 | input = new URL('wasm_mod_bg.wasm', import.meta.url); 110 | } 111 | const imports = __wbg_get_imports(); 112 | 113 | if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { 114 | input = fetch(input); 115 | } 116 | 117 | __wbg_init_memory(imports); 118 | 119 | const { instance, module } = await __wbg_load(await input, imports); 120 | 121 | return __wbg_finalize_init(instance, module); 122 | } 123 | 124 | export { initSync } 125 | export default __wbg_init; 126 | --------------------------------------------------------------------------------