Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support undefined static imports with Option #4319

Merged
merged 6 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Loading