Skip to content

Commit

Permalink
Support undefined static imports with Option (#4319)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Schmidt <[email protected]>
  • Loading branch information
daxpedda and RunDevelopment authored Dec 6, 2024
1 parent 4e77a61 commit 89f2af8
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 31 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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!`.
Expand Down
30 changes: 27 additions & 3 deletions crates/cli-support/src/js/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2578,6 +2578,30 @@ __wbg_set_wasm(wasm);"
Ok(name)
}

fn import_static(&mut self, import: &JsImport, optional: bool) -> Result<String, Error> {
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`.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) => {
Expand Down
7 changes: 5 additions & 2 deletions crates/cli-support/src/wit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(())
}

Expand Down
2 changes: 1 addition & 1 deletion crates/cli-support/src/wit/nonstandard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
3 changes: 3 additions & 0 deletions crates/cli/tests/reference/static.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* tslint:disable */
/* eslint-disable */
export function exported(): void;
85 changes: 85 additions & 0 deletions crates/cli/tests/reference/static.js
Original file line number Diff line number Diff line change
@@ -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));
};

30 changes: 30 additions & 0 deletions crates/cli/tests/reference/static.rs
Original file line number Diff line number Diff line change
@@ -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<Number>;
#[wasm_bindgen(thread_local_v2, js_namespace = test)]
static NAMESPACE_PLAIN: JsValue;
#[wasm_bindgen(thread_local_v2, js_namespace = test)]
static NAMESPACE_OPTIONAL: Option<Number>;
#[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<Number>;
}

#[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);
}
16 changes: 16 additions & 0 deletions crates/cli/tests/reference/static.wat
Original file line number Diff line number Diff line change
@@ -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")
)

35 changes: 14 additions & 21 deletions crates/js-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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<Object, JsValue>;
#[wasm_bindgen(thread_local_v2, js_name = globalThis)]
static GLOBAL_THIS: Option<Object>;

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

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

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

// 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;
}
Expand Down
26 changes: 24 additions & 2 deletions guide/src/reference/static-js-objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<T>` to handle this:

```rust
extern "C" {
type Crypto;
#[wasm_bindgen(thread_local_v2, js_name = crypto)]
static CRYPTO: Option<Crypto>;
}
```

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.
Expand Down
16 changes: 16 additions & 0 deletions tests/wasm/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>;

#[wasm_bindgen(thread_local_v2, js_namespace = test)]
static UNDECLARED_NAMESPACE: Option<u32>;

#[wasm_bindgen(thread_local_v2, js_namespace = ["test1", "test2"])]
static UNDECLARED_NESTED_NAMESPACE: Option<u32>;
}

#[wasm_bindgen(module = "tests/wasm/imports_2.js")]
Expand Down Expand Up @@ -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);
}

0 comments on commit 89f2af8

Please sign in to comment.