Skip to content

Commit 89f2af8

Browse files
Support undefined static imports with Option (#4319)
Co-authored-by: Michael Schmidt <[email protected]>
1 parent 4e77a61 commit 89f2af8

File tree

11 files changed

+226
-31
lines changed

11 files changed

+226
-31
lines changed

CHANGELOG.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88
* Add support for multi-threading in Node.js.
99
[#4318](https://github.com/rustwasm/wasm-bindgen/pull/4318)
1010

11-
### Changed
12-
1311
* Add clear error message to communicate new feature resolver version requirements.
1412
[#4312](https://github.com/rustwasm/wasm-bindgen/pull/4312)
1513

1614
* Remove `once_cell/critical-section` requirement for `no_std` with atomics.
1715
[#4322](https://github.com/rustwasm/wasm-bindgen/pull/4322)
1816

17+
### Changed
18+
19+
* `static FOO: Option<T>` now returns `None` if undeclared in JS instead of throwing an error in JS.
20+
[#4319](https://github.com/rustwasm/wasm-bindgen/pull/4319)
21+
1922
### Fixed
2023

2124
* Fix macro-hygiene for calls to `std::thread_local!`.

crates/cli-support/src/js/mod.rs

+27-3
Original file line numberDiff line numberDiff line change
@@ -2578,6 +2578,30 @@ __wbg_set_wasm(wasm);"
25782578
Ok(name)
25792579
}
25802580

2581+
fn import_static(&mut self, import: &JsImport, optional: bool) -> Result<String, Error> {
2582+
let mut name = self.import_name(&JsImport {
2583+
name: import.name.clone(),
2584+
fields: Vec::new(),
2585+
})?;
2586+
2587+
// After we've got an actual name handle field projections
2588+
if optional {
2589+
name = format!("typeof {name} === 'undefined' ? null : {name}");
2590+
2591+
for field in import.fields.iter() {
2592+
name.push_str("?.");
2593+
name.push_str(field);
2594+
}
2595+
} else {
2596+
for field in import.fields.iter() {
2597+
name.push('.');
2598+
name.push_str(field);
2599+
}
2600+
}
2601+
2602+
Ok(name)
2603+
}
2604+
25812605
/// If a start function is present, it removes it from the `start` section
25822606
/// of the Wasm module and then moves it to an exported function, named
25832607
/// `__wbindgen_start`.
@@ -2730,7 +2754,7 @@ __wbg_set_wasm(wasm);"
27302754
| AuxImport::Value(AuxValue::Setter(js, ..))
27312755
| AuxImport::ValueWithThis(js, ..)
27322756
| AuxImport::Instanceof(js)
2733-
| AuxImport::Static(js)
2757+
| AuxImport::Static { js, .. }
27342758
| AuxImport::StructuralClassGetter(js, ..)
27352759
| AuxImport::StructuralClassSetter(js, ..)
27362760
| AuxImport::IndexingGetterOfClass(js)
@@ -3265,11 +3289,11 @@ __wbg_set_wasm(wasm);"
32653289
Ok("result".to_owned())
32663290
}
32673291

3268-
AuxImport::Static(js) => {
3292+
AuxImport::Static { js, optional } => {
32693293
assert!(kind == AdapterJsImportKind::Normal);
32703294
assert!(!variadic);
32713295
assert_eq!(args.len(), 0);
3272-
self.import_name(js)
3296+
self.import_static(js, *optional)
32733297
}
32743298

32753299
AuxImport::String(string) => {

crates/cli-support/src/wit/mod.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,7 @@ impl<'a> Context<'a> {
788788
None => return Ok(()),
789789
Some(d) => d,
790790
};
791+
let optional = matches!(descriptor, Descriptor::Option(_));
791792

792793
// Register the signature of this imported shim
793794
let id = self.import_adapter(
@@ -803,8 +804,10 @@ impl<'a> Context<'a> {
803804

804805
// And then save off that this function is is an instanceof shim for an
805806
// imported item.
806-
let import = self.determine_import(import, static_.name)?;
807-
self.aux.import_map.insert(id, AuxImport::Static(import));
807+
let js = self.determine_import(import, static_.name)?;
808+
self.aux
809+
.import_map
810+
.insert(id, AuxImport::Static { js, optional });
808811
Ok(())
809812
}
810813

crates/cli-support/src/wit/nonstandard.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ pub enum AuxImport {
233233

234234
/// This import is expected to be a shim that returns the JS value named by
235235
/// `JsImport`.
236-
Static(JsImport),
236+
Static { js: JsImport, optional: bool },
237237

238238
/// This import is expected to be a shim that returns an exported `JsString`.
239239
String(String),
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/* tslint:disable */
2+
/* eslint-disable */
3+
export function exported(): void;

crates/cli/tests/reference/static.js

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
let wasm;
2+
export function __wbg_set_wasm(val) {
3+
wasm = val;
4+
}
5+
6+
7+
function isLikeNone(x) {
8+
return x === undefined || x === null;
9+
}
10+
11+
function addToExternrefTable0(obj) {
12+
const idx = wasm.__externref_table_alloc();
13+
wasm.__wbindgen_export_1.set(idx, obj);
14+
return idx;
15+
}
16+
17+
const lTextDecoder = typeof TextDecoder === 'undefined' ? (0, module.require)('util').TextDecoder : TextDecoder;
18+
19+
let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true });
20+
21+
cachedTextDecoder.decode();
22+
23+
let cachedUint8ArrayMemory0 = null;
24+
25+
function getUint8ArrayMemory0() {
26+
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
27+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
28+
}
29+
return cachedUint8ArrayMemory0;
30+
}
31+
32+
function getStringFromWasm0(ptr, len) {
33+
ptr = ptr >>> 0;
34+
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
35+
}
36+
37+
export function exported() {
38+
wasm.exported();
39+
}
40+
41+
export function __wbg_static_accessor_NAMESPACE_OPTIONAL_c9a4344c544120f4() {
42+
const ret = typeof test === 'undefined' ? null : test?.NAMESPACE_OPTIONAL;
43+
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
44+
};
45+
46+
export function __wbg_static_accessor_NAMESPACE_PLAIN_784c8d7f5bbac62a() {
47+
const ret = test.NAMESPACE_PLAIN;
48+
return ret;
49+
};
50+
51+
export function __wbg_static_accessor_NESTED_NAMESPACE_OPTIONAL_a414abbeb018a35a() {
52+
const ret = typeof test1 === 'undefined' ? null : test1?.test2?.NESTED_NAMESPACE_OPTIONAL;
53+
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
54+
};
55+
56+
export function __wbg_static_accessor_NESTED_NAMESPACE_PLAIN_1121b285cb8479df() {
57+
const ret = test1.test2.NESTED_NAMESPACE_PLAIN;
58+
return ret;
59+
};
60+
61+
export function __wbg_static_accessor_OPTIONAL_ade71b6402851d0c() {
62+
const ret = typeof OPTIONAL === 'undefined' ? null : OPTIONAL;
63+
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
64+
};
65+
66+
export function __wbg_static_accessor_PLAIN_c0f08eb2f0db194c() {
67+
const ret = PLAIN;
68+
return ret;
69+
};
70+
71+
export function __wbindgen_init_externref_table() {
72+
const table = wasm.__wbindgen_export_1;
73+
const offset = table.grow(4);
74+
table.set(0, undefined);
75+
table.set(offset + 0, undefined);
76+
table.set(offset + 1, null);
77+
table.set(offset + 2, true);
78+
table.set(offset + 3, false);
79+
;
80+
};
81+
82+
export function __wbindgen_throw(arg0, arg1) {
83+
throw new Error(getStringFromWasm0(arg0, arg1));
84+
};
85+

crates/cli/tests/reference/static.rs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// DEPENDENCY: js-sys = { path = '{root}/crates/js-sys' }
2+
3+
use wasm_bindgen::prelude::*;
4+
use js_sys::Number;
5+
6+
#[wasm_bindgen]
7+
extern "C" {
8+
#[wasm_bindgen(thread_local_v2)]
9+
static PLAIN: JsValue;
10+
#[wasm_bindgen(thread_local_v2)]
11+
static OPTIONAL: Option<Number>;
12+
#[wasm_bindgen(thread_local_v2, js_namespace = test)]
13+
static NAMESPACE_PLAIN: JsValue;
14+
#[wasm_bindgen(thread_local_v2, js_namespace = test)]
15+
static NAMESPACE_OPTIONAL: Option<Number>;
16+
#[wasm_bindgen(thread_local_v2, js_namespace = ["test1", "test2"])]
17+
static NESTED_NAMESPACE_PLAIN: JsValue;
18+
#[wasm_bindgen(thread_local_v2, js_namespace = ["test1", "test2"])]
19+
static NESTED_NAMESPACE_OPTIONAL: Option<Number>;
20+
}
21+
22+
#[wasm_bindgen]
23+
pub fn exported() {
24+
let _ = PLAIN.with(JsValue::clone);
25+
let _ = OPTIONAL.with(Option::clone);
26+
let _ = NAMESPACE_PLAIN.with(JsValue::clone);
27+
let _ = NAMESPACE_OPTIONAL.with(Option::clone);
28+
let _ = NESTED_NAMESPACE_PLAIN.with(JsValue::clone);
29+
let _ = NESTED_NAMESPACE_OPTIONAL.with(Option::clone);
30+
}

crates/cli/tests/reference/static.wat

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
(module $reference_test.wasm
2+
(type (;0;) (func))
3+
(type (;1;) (func (result i32)))
4+
(import "./reference_test_bg.js" "__wbindgen_init_externref_table" (func (;0;) (type 0)))
5+
(func $__externref_table_alloc (;1;) (type 1) (result i32))
6+
(func $exported (;2;) (type 0))
7+
(table (;0;) 128 externref)
8+
(memory (;0;) 17)
9+
(export "memory" (memory 0))
10+
(export "exported" (func $exported))
11+
(export "__externref_table_alloc" (func $__externref_table_alloc))
12+
(export "__wbindgen_export_1" (table 0))
13+
(export "__wbindgen_start" (func 0))
14+
(@custom "target_features" (after code) "\04+\0amultivalue+\0fmutable-globals+\0freference-types+\08sign-ext")
15+
)
16+

crates/js-sys/src/lib.rs

+14-21
Original file line numberDiff line numberDiff line change
@@ -6055,14 +6055,6 @@ pub fn global() -> Object {
60556055
}
60566056

60576057
fn get_global_object() -> Object {
6058-
// This is a bit wonky, but we're basically using `#[wasm_bindgen]`
6059-
// attributes to synthesize imports so we can access properties of the
6060-
// form:
6061-
//
6062-
// * `globalThis.globalThis`
6063-
// * `self.self`
6064-
// * ... (etc)
6065-
//
60666058
// Accessing the global object is not an easy thing to do, and what we
60676059
// basically want is `globalThis` but we can't rely on that existing
60686060
// everywhere. In the meantime we've got the fallbacks mentioned in:
@@ -6076,26 +6068,27 @@ pub fn global() -> Object {
60766068
extern "C" {
60776069
type Global;
60786070

6079-
#[wasm_bindgen(getter, catch, static_method_of = Global, js_class = globalThis, js_name = globalThis)]
6080-
fn get_global_this() -> Result<Object, JsValue>;
6071+
#[wasm_bindgen(thread_local_v2, js_name = globalThis)]
6072+
static GLOBAL_THIS: Option<Object>;
60816073

6082-
#[wasm_bindgen(getter, catch, static_method_of = Global, js_class = self, js_name = self)]
6083-
fn get_self() -> Result<Object, JsValue>;
6074+
#[wasm_bindgen(thread_local_v2, js_name = self)]
6075+
static SELF: Option<Object>;
60846076

6085-
#[wasm_bindgen(getter, catch, static_method_of = Global, js_class = window, js_name = window)]
6086-
fn get_window() -> Result<Object, JsValue>;
6077+
#[wasm_bindgen(thread_local_v2, js_name = window)]
6078+
static WINDOW: Option<Object>;
60876079

6088-
#[wasm_bindgen(getter, catch, static_method_of = Global, js_class = global, js_name = global)]
6089-
fn get_global() -> Result<Object, JsValue>;
6080+
#[wasm_bindgen(thread_local_v2, js_name = global)]
6081+
static GLOBAL: Option<Object>;
60906082
}
60916083

60926084
// The order is important: in Firefox Extension Content Scripts `globalThis`
60936085
// is a Sandbox (not Window), so `globalThis` must be checked after `window`.
6094-
let static_object = Global::get_self()
6095-
.or_else(|_| Global::get_window())
6096-
.or_else(|_| Global::get_global_this())
6097-
.or_else(|_| Global::get_global());
6098-
if let Ok(obj) = static_object {
6086+
let static_object = SELF
6087+
.with(Option::clone)
6088+
.or_else(|| WINDOW.with(Option::clone))
6089+
.or_else(|| GLOBAL_THIS.with(Option::clone))
6090+
.or_else(|| GLOBAL.with(Option::clone));
6091+
if let Some(obj) = static_object {
60996092
if !obj.is_undefined() {
61006093
return obj;
61016094
}

guide/src/reference/static-js-objects.md

+24-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ JavaScript modules will often export arbitrary static objects for use with
44
their provided interfaces. These objects can be accessed from Rust by declaring
55
a named `static` in the `extern` block with an
66
`#[wasm_bindgen(thread_local_v2)]` attribute. `wasm-bindgen` will bind a
7-
`JsThreadLocal` for these objects, which can be cloned into a `JsValue`. For
8-
example, given the following JavaScript:
7+
`JsThreadLocal` for these objects, which can be cloned into a `JsValue`.
8+
9+
These values are cached in a thread-local and are meant to bind static values
10+
or objects only. For getters which can change their return value or throw see
11+
[how to import getters](attributes/on-js-imports/getter-and-setter.md).
12+
13+
For example, given the following JavaScript:
914

1015
```js
1116
let COLORS = {
@@ -65,6 +70,23 @@ extern "C" {
6570
}
6671
```
6772

73+
## Optional statics
74+
75+
If you expect the JavaScript value you're trying to access to not always be
76+
available you can use `Option<T>` to handle this:
77+
78+
```rust
79+
extern "C" {
80+
type Crypto;
81+
#[wasm_bindgen(thread_local_v2, js_name = crypto)]
82+
static CRYPTO: Option<Crypto>;
83+
}
84+
```
85+
86+
If `crypto` is not declared or nullish (`null` or `undefined`) in JavaScript,
87+
it will simply return `None` in Rust. This will also account for namespaces: it
88+
will return `Some(T)` only if all parts are declared and not nullish.
89+
6890
## Static strings
6991

7092
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.

tests/wasm/imports.rs

+16
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ extern "C" {
9090

9191
#[wasm_bindgen(js_name = "\"string'literal\nbreakers\r")]
9292
fn string_literal_breakers() -> u32;
93+
94+
#[wasm_bindgen(thread_local_v2)]
95+
static UNDECLARED: Option<u32>;
96+
97+
#[wasm_bindgen(thread_local_v2, js_namespace = test)]
98+
static UNDECLARED_NAMESPACE: Option<u32>;
99+
100+
#[wasm_bindgen(thread_local_v2, js_namespace = ["test1", "test2"])]
101+
static UNDECLARED_NESTED_NAMESPACE: Option<u32>;
93102
}
94103

95104
#[wasm_bindgen(module = "tests/wasm/imports_2.js")]
@@ -336,3 +345,10 @@ fn invalid_idents() {
336345
assert_eq!(kebab_case(), 42);
337346
assert_eq!(string_literal_breakers(), 42);
338347
}
348+
349+
#[wasm_bindgen_test]
350+
fn undeclared() {
351+
assert_eq!(UNDECLARED.with(Option::clone), None);
352+
assert_eq!(UNDECLARED_NAMESPACE.with(Option::clone), None);
353+
assert_eq!(UNDECLARED_NESTED_NAMESPACE.with(Option::clone), None);
354+
}

0 commit comments

Comments
 (0)