diff --git a/CHANGELOG.md b/CHANGELOG.md index dece4a6063d..7181e5310c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,17 @@ * Add support for multi-threading in Node.js. [#4318](https://github.com/rustwasm/wasm-bindgen/pull/4318) -### Changed - * Add clear error message to communicate new feature resolver version requirements. [#4312](https://github.com/rustwasm/wasm-bindgen/pull/4312) * Remove `once_cell/critical-section` requirement for `no_std` with atomics. [#4322](https://github.com/rustwasm/wasm-bindgen/pull/4322) +### Changed + +* `static FOO: Option` now returns `None` if undeclared in JS instead of throwing an error in JS. + [#4319](https://github.com/rustwasm/wasm-bindgen/pull/4319) + ### Fixed * Fix macro-hygiene for calls to `std::thread_local!`. diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 21139dddab3..e26f7a6d2ca 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -2578,6 +2578,30 @@ __wbg_set_wasm(wasm);" Ok(name) } + fn import_static(&mut self, import: &JsImport, optional: bool) -> Result { + let mut name = self.import_name(&JsImport { + name: import.name.clone(), + fields: Vec::new(), + })?; + + // After we've got an actual name handle field projections + if optional { + name = format!("typeof {name} === 'undefined' ? null : {name}"); + + for field in import.fields.iter() { + name.push_str("?."); + name.push_str(field); + } + } else { + for field in import.fields.iter() { + name.push('.'); + name.push_str(field); + } + } + + Ok(name) + } + /// If a start function is present, it removes it from the `start` section /// of the Wasm module and then moves it to an exported function, named /// `__wbindgen_start`. @@ -2730,7 +2754,7 @@ __wbg_set_wasm(wasm);" | AuxImport::Value(AuxValue::Setter(js, ..)) | AuxImport::ValueWithThis(js, ..) | AuxImport::Instanceof(js) - | AuxImport::Static(js) + | AuxImport::Static { js, .. } | AuxImport::StructuralClassGetter(js, ..) | AuxImport::StructuralClassSetter(js, ..) | AuxImport::IndexingGetterOfClass(js) @@ -3265,11 +3289,11 @@ __wbg_set_wasm(wasm);" Ok("result".to_owned()) } - AuxImport::Static(js) => { + AuxImport::Static { js, optional } => { assert!(kind == AdapterJsImportKind::Normal); assert!(!variadic); assert_eq!(args.len(), 0); - self.import_name(js) + self.import_static(js, *optional) } AuxImport::String(string) => { diff --git a/crates/cli-support/src/wit/mod.rs b/crates/cli-support/src/wit/mod.rs index 2f1d7f52691..07fb5090a78 100644 --- a/crates/cli-support/src/wit/mod.rs +++ b/crates/cli-support/src/wit/mod.rs @@ -788,6 +788,7 @@ impl<'a> Context<'a> { None => return Ok(()), Some(d) => d, }; + let optional = matches!(descriptor, Descriptor::Option(_)); // Register the signature of this imported shim let id = self.import_adapter( @@ -803,8 +804,10 @@ impl<'a> Context<'a> { // And then save off that this function is is an instanceof shim for an // imported item. - let import = self.determine_import(import, static_.name)?; - self.aux.import_map.insert(id, AuxImport::Static(import)); + let js = self.determine_import(import, static_.name)?; + self.aux + .import_map + .insert(id, AuxImport::Static { js, optional }); Ok(()) } diff --git a/crates/cli-support/src/wit/nonstandard.rs b/crates/cli-support/src/wit/nonstandard.rs index 35f30a74497..1e9579dc8ba 100644 --- a/crates/cli-support/src/wit/nonstandard.rs +++ b/crates/cli-support/src/wit/nonstandard.rs @@ -233,7 +233,7 @@ pub enum AuxImport { /// This import is expected to be a shim that returns the JS value named by /// `JsImport`. - Static(JsImport), + Static { js: JsImport, optional: bool }, /// This import is expected to be a shim that returns an exported `JsString`. String(String), diff --git a/crates/cli/tests/reference/static.d.ts b/crates/cli/tests/reference/static.d.ts new file mode 100644 index 00000000000..091dbcc51a6 --- /dev/null +++ b/crates/cli/tests/reference/static.d.ts @@ -0,0 +1,3 @@ +/* tslint:disable */ +/* eslint-disable */ +export function exported(): void; diff --git a/crates/cli/tests/reference/static.js b/crates/cli/tests/reference/static.js new file mode 100644 index 00000000000..34aa7975e7a --- /dev/null +++ b/crates/cli/tests/reference/static.js @@ -0,0 +1,85 @@ +let wasm; +export function __wbg_set_wasm(val) { + wasm = val; +} + + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_export_1.set(idx, obj); + return idx; +} + +const lTextDecoder = typeof TextDecoder === 'undefined' ? (0, module.require)('util').TextDecoder : TextDecoder; + +let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +let cachedUint8ArrayMemory0 = null; + +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +export function exported() { + wasm.exported(); +} + +export function __wbg_static_accessor_NAMESPACE_OPTIONAL_c9a4344c544120f4() { + const ret = typeof test === 'undefined' ? null : test?.NAMESPACE_OPTIONAL; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); +}; + +export function __wbg_static_accessor_NAMESPACE_PLAIN_784c8d7f5bbac62a() { + const ret = test.NAMESPACE_PLAIN; + return ret; +}; + +export function __wbg_static_accessor_NESTED_NAMESPACE_OPTIONAL_a414abbeb018a35a() { + const ret = typeof test1 === 'undefined' ? null : test1?.test2?.NESTED_NAMESPACE_OPTIONAL; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); +}; + +export function __wbg_static_accessor_NESTED_NAMESPACE_PLAIN_1121b285cb8479df() { + const ret = test1.test2.NESTED_NAMESPACE_PLAIN; + return ret; +}; + +export function __wbg_static_accessor_OPTIONAL_ade71b6402851d0c() { + const ret = typeof OPTIONAL === 'undefined' ? null : OPTIONAL; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); +}; + +export function __wbg_static_accessor_PLAIN_c0f08eb2f0db194c() { + const ret = PLAIN; + return ret; +}; + +export function __wbindgen_init_externref_table() { + const table = wasm.__wbindgen_export_1; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + ; +}; + +export function __wbindgen_throw(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +}; + diff --git a/crates/cli/tests/reference/static.rs b/crates/cli/tests/reference/static.rs new file mode 100644 index 00000000000..4437f550e7d --- /dev/null +++ b/crates/cli/tests/reference/static.rs @@ -0,0 +1,30 @@ +// DEPENDENCY: js-sys = { path = '{root}/crates/js-sys' } + +use wasm_bindgen::prelude::*; +use js_sys::Number; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(thread_local_v2)] + static PLAIN: JsValue; + #[wasm_bindgen(thread_local_v2)] + static OPTIONAL: Option; + #[wasm_bindgen(thread_local_v2, js_namespace = test)] + static NAMESPACE_PLAIN: JsValue; + #[wasm_bindgen(thread_local_v2, js_namespace = test)] + static NAMESPACE_OPTIONAL: Option; + #[wasm_bindgen(thread_local_v2, js_namespace = ["test1", "test2"])] + static NESTED_NAMESPACE_PLAIN: JsValue; + #[wasm_bindgen(thread_local_v2, js_namespace = ["test1", "test2"])] + static NESTED_NAMESPACE_OPTIONAL: Option; +} + +#[wasm_bindgen] +pub fn exported() { + let _ = PLAIN.with(JsValue::clone); + let _ = OPTIONAL.with(Option::clone); + let _ = NAMESPACE_PLAIN.with(JsValue::clone); + let _ = NAMESPACE_OPTIONAL.with(Option::clone); + let _ = NESTED_NAMESPACE_PLAIN.with(JsValue::clone); + let _ = NESTED_NAMESPACE_OPTIONAL.with(Option::clone); +} diff --git a/crates/cli/tests/reference/static.wat b/crates/cli/tests/reference/static.wat new file mode 100644 index 00000000000..b4ea3e1c9e8 --- /dev/null +++ b/crates/cli/tests/reference/static.wat @@ -0,0 +1,16 @@ +(module $reference_test.wasm + (type (;0;) (func)) + (type (;1;) (func (result i32))) + (import "./reference_test_bg.js" "__wbindgen_init_externref_table" (func (;0;) (type 0))) + (func $__externref_table_alloc (;1;) (type 1) (result i32)) + (func $exported (;2;) (type 0)) + (table (;0;) 128 externref) + (memory (;0;) 17) + (export "memory" (memory 0)) + (export "exported" (func $exported)) + (export "__externref_table_alloc" (func $__externref_table_alloc)) + (export "__wbindgen_export_1" (table 0)) + (export "__wbindgen_start" (func 0)) + (@custom "target_features" (after code) "\04+\0amultivalue+\0fmutable-globals+\0freference-types+\08sign-ext") +) + diff --git a/crates/js-sys/src/lib.rs b/crates/js-sys/src/lib.rs index 57b01303cd4..aa2f3a07b3b 100644 --- a/crates/js-sys/src/lib.rs +++ b/crates/js-sys/src/lib.rs @@ -6055,14 +6055,6 @@ pub fn global() -> Object { } fn get_global_object() -> Object { - // This is a bit wonky, but we're basically using `#[wasm_bindgen]` - // attributes to synthesize imports so we can access properties of the - // form: - // - // * `globalThis.globalThis` - // * `self.self` - // * ... (etc) - // // Accessing the global object is not an easy thing to do, and what we // basically want is `globalThis` but we can't rely on that existing // everywhere. In the meantime we've got the fallbacks mentioned in: @@ -6076,26 +6068,27 @@ pub fn global() -> Object { extern "C" { type Global; - #[wasm_bindgen(getter, catch, static_method_of = Global, js_class = globalThis, js_name = globalThis)] - fn get_global_this() -> Result; + #[wasm_bindgen(thread_local_v2, js_name = globalThis)] + static GLOBAL_THIS: Option; - #[wasm_bindgen(getter, catch, static_method_of = Global, js_class = self, js_name = self)] - fn get_self() -> Result; + #[wasm_bindgen(thread_local_v2, js_name = self)] + static SELF: Option; - #[wasm_bindgen(getter, catch, static_method_of = Global, js_class = window, js_name = window)] - fn get_window() -> Result; + #[wasm_bindgen(thread_local_v2, js_name = window)] + static WINDOW: Option; - #[wasm_bindgen(getter, catch, static_method_of = Global, js_class = global, js_name = global)] - fn get_global() -> Result; + #[wasm_bindgen(thread_local_v2, js_name = global)] + static GLOBAL: Option; } // The order is important: in Firefox Extension Content Scripts `globalThis` // is a Sandbox (not Window), so `globalThis` must be checked after `window`. - let static_object = Global::get_self() - .or_else(|_| Global::get_window()) - .or_else(|_| Global::get_global_this()) - .or_else(|_| Global::get_global()); - if let Ok(obj) = static_object { + let static_object = SELF + .with(Option::clone) + .or_else(|| WINDOW.with(Option::clone)) + .or_else(|| GLOBAL_THIS.with(Option::clone)) + .or_else(|| GLOBAL.with(Option::clone)); + if let Some(obj) = static_object { if !obj.is_undefined() { return obj; } diff --git a/guide/src/reference/static-js-objects.md b/guide/src/reference/static-js-objects.md index b6aee687dc8..92b699d57c6 100644 --- a/guide/src/reference/static-js-objects.md +++ b/guide/src/reference/static-js-objects.md @@ -4,8 +4,13 @@ JavaScript modules will often export arbitrary static objects for use with their provided interfaces. These objects can be accessed from Rust by declaring a named `static` in the `extern` block with an `#[wasm_bindgen(thread_local_v2)]` attribute. `wasm-bindgen` will bind a -`JsThreadLocal` for these objects, which can be cloned into a `JsValue`. For -example, given the following JavaScript: +`JsThreadLocal` for these objects, which can be cloned into a `JsValue`. + +These values are cached in a thread-local and are meant to bind static values +or objects only. For getters which can change their return value or throw see +[how to import getters](attributes/on-js-imports/getter-and-setter.md). + +For example, given the following JavaScript: ```js let COLORS = { @@ -65,6 +70,23 @@ extern "C" { } ``` +## Optional statics + +If you expect the JavaScript value you're trying to access to not always be +available you can use `Option` to handle this: + +```rust +extern "C" { + type Crypto; + #[wasm_bindgen(thread_local_v2, js_name = crypto)] + static CRYPTO: Option; +} +``` + +If `crypto` is not declared or nullish (`null` or `undefined`) in JavaScript, +it will simply return `None` in Rust. This will also account for namespaces: it +will return `Some(T)` only if all parts are declared and not nullish. + ## Static strings Strings can be imported to avoid going through `TextDecoder/Encoder` when requiring just a `JsString`. This can be useful when dealing with environments where `TextDecoder/Encoder` is not available, like in audio worklets. diff --git a/tests/wasm/imports.rs b/tests/wasm/imports.rs index a6eab0c5e32..119aefef487 100644 --- a/tests/wasm/imports.rs +++ b/tests/wasm/imports.rs @@ -90,6 +90,15 @@ extern "C" { #[wasm_bindgen(js_name = "\"string'literal\nbreakers\r")] fn string_literal_breakers() -> u32; + + #[wasm_bindgen(thread_local_v2)] + static UNDECLARED: Option; + + #[wasm_bindgen(thread_local_v2, js_namespace = test)] + static UNDECLARED_NAMESPACE: Option; + + #[wasm_bindgen(thread_local_v2, js_namespace = ["test1", "test2"])] + static UNDECLARED_NESTED_NAMESPACE: Option; } #[wasm_bindgen(module = "tests/wasm/imports_2.js")] @@ -336,3 +345,10 @@ fn invalid_idents() { assert_eq!(kebab_case(), 42); assert_eq!(string_literal_breakers(), 42); } + +#[wasm_bindgen_test] +fn undeclared() { + assert_eq!(UNDECLARED.with(Option::clone), None); + assert_eq!(UNDECLARED_NAMESPACE.with(Option::clone), None); + assert_eq!(UNDECLARED_NESTED_NAMESPACE.with(Option::clone), None); +}