Repository: rustwasm/wasm-bindgen
Pull request #4057 (2024-08-06 21:33:10)

Deprecate `JsStatic` in favor of `#[wasm_bindgen(thread_local)]`
This PR deprecated `JsStatic` in favor of `#[wasm_bindgen(thread_local)]`, which creates a `std::thread::LocalKey`. The syntax is otherwise the same.

```rust
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(thread_local)]
    static FOO: JsValue;
}
```

`JsStatic` was pretty much a hack that exposed a thread local variable by transmuting the lifetime to `'static`, which does make sense on Web, but not with multi-threading, which was making this rather unsound.

Additionally, `impl Deref for JsStatic` is not present anymore when compiling with `cfg(target_feature = "atomics")` to remove the possibility of unsoundness entirely.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9f9997d..df7ad8a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -72,7 +72,7 @@
 * Added an experimental Node.JS ES module target, in comparison the current `node` target uses CommonJS, with `--target experimental-nodejs-module` or when testing with `wasm_bindgen_test_configure!(run_in_node_experimental)`.
   [#4027](https://github.com/rustwasm/wasm-bindgen/pull/4027)
 
-* Added importing strings as `JsString` through `#[wasm_bindgen(static_string)] static STRING: JsString = "a string literal";`.
+* Added importing strings as `JsString` through `#[wasm_bindgen(thread_local, static_string)] static STRING: JsString = "a string literal";`.
   [#4055](https://github.com/rustwasm/wasm-bindgen/pull/4055)
 
 ### Changed
@@ -129,6 +129,12 @@
 * Filtered files in published crates, significantly reducing the package size and notably excluding any bash files.
   [#4046](https://github.com/rustwasm/wasm-bindgen/pull/4046)
 
+* Deprecated `JsStatic` in favor of `#[wasm_bindgen(thread_local)]`, which creates a `std::thread::LocalKey`. The syntax is otherwise the same.
+  [#4057](https://github.com/rustwasm/wasm-bindgen/pull/4057)
+
+* Removed `impl Deref for JsStatic` when compiling with `cfg(target_feature = "atomics")`, which was unsound.
+  [#4057](https://github.com/rustwasm/wasm-bindgen/pull/4057)
+
 ### Fixed
 
 * Copy port from headless test server when using `WASM_BINDGEN_TEST_ADDRESS`.
diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs
index 3474d3a5..e10cf756 100644
--- a/crates/backend/src/ast.rs
+++ b/crates/backend/src/ast.rs
@@ -275,6 +275,8 @@ pub struct ImportStatic {
     pub js_name: String,
     /// Path to wasm_bindgen
     pub wasm_bindgen: Path,
+    /// [`true`] if using the new `thread_local` representation.
+    pub thread_local: bool,
 }
 
 /// The type of a static string being imported
diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs
index c826d67c..4add9859 100644
--- a/crates/backend/src/codegen.rs
+++ b/crates/backend/src/codegen.rs
@@ -1645,15 +1645,42 @@ impl ToTokens for ast::Enum {
 impl ToTokens for ast::ImportStatic {
     fn to_tokens(&self, into: &mut TokenStream) {
         let ty = &self.ty;
-        static_import(
-            &self.vis,
-            &self.rust_name,
-            &self.wasm_bindgen,
-            ty,
-            ty,
-            &self.shim,
-        )
-        .to_tokens(into);
+
+        if self.thread_local {
+            thread_local_import(
+                &self.vis,
+                &self.rust_name,
+                &self.wasm_bindgen,
+                ty,
+                ty,
+                &self.shim,
+            )
+            .to_tokens(into)
+        } else {
+            let vis = &self.vis;
+            let name = &self.rust_name;
+            let wasm_bindgen = &self.wasm_bindgen;
+            let ty = &self.ty;
+            let shim_name = &self.shim;
+            let init = static_init(wasm_bindgen, ty, shim_name);
+
+            into.extend(quote! {
+                #[automatically_derived]
+                #[deprecated = "use with `#[wasm_bindgen(thread_local)]` instead"]
+            });
+            into.extend(
+                quote_spanned! { name.span() => #vis static #name: #wasm_bindgen::JsStatic<#ty> = {
+                        fn init() -> #ty {
+                            #init
+                        }
+                        thread_local!(static _VAL: #ty = init(););
+                        #wasm_bindgen::JsStatic {
+                            __inner: &_VAL,
+                        }
+                    };
+                },
+            );
+        }
 
         Descriptor {
             ident: &self.shim,
@@ -1672,7 +1699,7 @@ impl ToTokens for ast::ImportString {
         let js_sys = &self.js_sys;
         let actual_ty: syn::Type = parse_quote!(#js_sys::JsString);
 
-        static_import(
+        thread_local_import(
             &self.vis,
             &self.rust_name,
             &self.wasm_bindgen,
@@ -1684,7 +1711,7 @@ impl ToTokens for ast::ImportString {
     }
 }
 
-fn static_import(
+fn thread_local_import(
     vis: &syn::Visibility,
     name: &Ident,
     wasm_bindgen: &syn::Path,
@@ -1692,33 +1719,37 @@ fn static_import(
     ty: &syn::Type,
     shim_name: &Ident,
 ) -> TokenStream {
+    let init = static_init(wasm_bindgen, ty, shim_name);
+
+    quote! {
+        thread_local! {
+            #[automatically_derived]
+            #vis static #name: #actual_ty = {
+                #init
+            };
+        }
+    }
+}
+
+fn static_init(wasm_bindgen: &syn::Path, ty: &syn::Type, shim_name: &Ident) -> TokenStream {
     let abi_ret = quote! {
         #wasm_bindgen::convert::WasmRet<<#ty as #wasm_bindgen::convert::FromWasmAbi>::Abi>
     };
     quote! {
-        #[automatically_derived]
-        #vis static #name: #wasm_bindgen::JsStatic<#actual_ty> = {
-            fn init() -> #ty {
-                #[link(wasm_import_module = "__wbindgen_placeholder__")]
-                #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
-                extern "C" {
-                    fn #shim_name() -> #abi_ret;
-                }
+        #[link(wasm_import_module = "__wbindgen_placeholder__")]
+        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
+        extern "C" {
+            fn #shim_name() -> #abi_ret;
+        }
 
-                #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
-                unsafe fn #shim_name() -> #abi_ret {
-                    panic!("cannot access imported statics on non-wasm targets")
-                }
+        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
+        unsafe fn #shim_name() -> #abi_ret {
+            panic!("cannot access imported statics on non-wasm targets")
+        }
 
-                unsafe {
-                    <#ty as #wasm_bindgen::convert::FromWasmAbi>::from_abi(#shim_name().join())
-                }
-            }
-            thread_local!(static _VAL: #ty = init(););
-            #wasm_bindgen::JsStatic {
-                __inner: &_VAL,
-            }
-        };
+        unsafe {
+            <#ty as #wasm_bindgen::convert::FromWasmAbi>::from_abi(#shim_name().join())
+        }
     }
 }
 
diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs
index 3e3f3113..bcc4d739 100644
--- a/crates/macro-support/src/parser.rs
+++ b/crates/macro-support/src/parser.rs
@@ -93,6 +93,7 @@ macro_rules! attrgen {
             (typescript_type, TypeScriptType(Span, String, Span)),
             (getter_with_clone, GetterWithClone(Span)),
             (static_string, StaticString(Span)),
+            (thread_local, ThreadLocal(Span)),
 
             // For testing purposes only.
             (assert_no_shim, AssertNoShim(Span)),
@@ -753,6 +754,13 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule
             bail_span!(self.mutability, "cannot import mutable globals yet")
         }
 
+        if let Some(span) = opts.static_string() {
+            return Err(Diagnostic::span_error(
+                *span,
+                "static strings require a string literal",
+            ));
+        }
+
         let default_name = self.ident.to_string();
         let js_name = opts
             .js_name()
@@ -764,6 +772,8 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule
             self.ident,
             ShortHash((&js_name, module, &self.ident)),
         );
+        let thread_local = opts.thread_local().is_some();
+
         opts.check_used();
         Ok(ast::ImportKind::Static(ast::ImportStatic {
             ty: *self.ty,
@@ -772,6 +782,7 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule
             js_name,
             shim: Ident::new(&shim, Span::call_site()),
             wasm_bindgen: program.wasm_bindgen.clone(),
+            thread_local,
         }))
     }
 }
@@ -805,7 +816,14 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule
         if opts.static_string().is_none() {
             bail_span!(
                 self,
-                "statics strings require `#[wasm_bindgen(static_string)]`"
+                "static strings require `#[wasm_bindgen(static_string)]`"
+            )
+        }
+
+        if opts.thread_local().is_none() {
+            bail_span!(
+                self,
+                "static strings require `#[wasm_bindgen(thread_local)]`"
             )
         }
 
diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml
index cd04f2a6..e448a0d2 100644
--- a/crates/macro/Cargo.toml
+++ b/crates/macro/Cargo.toml
@@ -26,6 +26,7 @@ wasm-bindgen-macro-support = { path = "../macro-support", version = "=0.2.92" }
 quote = "1.0"
 
 [dev-dependencies]
+js-sys = { path = "../js-sys" }
 trybuild = "1.0"
 wasm-bindgen = { path = "../.." }
 wasm-bindgen-futures = { path = "../futures" }
diff --git a/examples/wasm-audio-worklet/src/dependent_module.rs b/examples/wasm-audio-worklet/src/dependent_module.rs
index ce7f038f..5e074d72 100644
--- a/examples/wasm-audio-worklet/src/dependent_module.rs
+++ b/examples/wasm-audio-worklet/src/dependent_module.rs
@@ -12,7 +12,7 @@ extern "C" {
     #[wasm_bindgen(method, getter)]
     fn url(this: &ImportMeta) -> JsString;
 
-    #[wasm_bindgen(js_namespace = import, js_name = meta)]
+    #[wasm_bindgen(thread_local, js_namespace = import, js_name = meta)]
     static IMPORT_META: ImportMeta;
 }
 
@@ -20,7 +20,7 @@ pub fn on_the_fly(code: &str) -> Result<String, JsValue> {
     // Generate the import of the bindgen ES module, assuming `--target web`.
     let header = format!(
         "import init, * as bindgen from '{}';\n\n",
-        IMPORT_META.url(),
+        IMPORT_META.with(ImportMeta::url),
     );
 
     let options = BlobPropertyBag::new();
diff --git a/guide/src/reference/attributes/on-js-imports/indexing-getter-setter-deleter.md b/guide/src/reference/attributes/on-js-imports/indexing-getter-setter-deleter.md
index 5dad7d46..0d0443cb 100644
--- a/guide/src/reference/attributes/on-js-imports/indexing-getter-setter-deleter.md
+++ b/guide/src/reference/attributes/on-js-imports/indexing-getter-setter-deleter.md
@@ -60,7 +60,8 @@ on methods:
 #[wasm_bindgen]
 extern "C" {
     type Foo;
-    static foo: Foo;
+    #[wasm_bindgen(thread_local)]
+    static FOO: Foo;
 
     #[wasm_bindgen(method, structural, indexing_getter)]
     fn get(this: &Foo, prop: &str) -> u32;
@@ -72,11 +73,13 @@ extern "C" {
     fn delete(this: &Foo, prop: &str);
 }
 
-assert_eq!(foo.get("ten"), 3);
+FOO.with(|foo| {
+    assert_eq!(foo.get("ten"), 3);
 
-foo.set("ten", 10);
-assert_eq!(foo.get("ten"), 10);
+    foo.set("ten", 10);
+    assert_eq!(foo.get("ten"), 10);
 
-foo.delete("ten");
-assert_eq!(foo.get("ten"), 3);
+    foo.delete("ten");
+    assert_eq!(foo.get("ten"), 3);
+});
 ```
diff --git a/guide/src/reference/static-js-objects.md b/guide/src/reference/static-js-objects.md
index 9c15fcbb..2402098e 100644
--- a/guide/src/reference/static-js-objects.md
+++ b/guide/src/reference/static-js-objects.md
@@ -19,11 +19,12 @@ let COLORS = {
 ```rust
 #[wasm_bindgen]
 extern "C" {
+    #[wasm_bindgen(thread_local)]
     static COLORS;
 }
 
 fn get_colors() -> JsValue {
-    COLORS.clone()
+    COLORS.with(JsValue::clone)
 }
 ```
 
@@ -49,11 +50,11 @@ The binding for this module:
 #[wasm_bindgen(module = "/js/some-rollup.js")]
 extern "C" {
     // Likewise with the namespace--this refers to the object directly.
-    #[wasm_bindgen(js_name = namespace)]
+    #[wasm_bindgen(thread_local, js_name = namespace)]
     static NAMESPACE: JsValue;
 
     // Refer to SomeType's class
-    #[wasm_bindgen(js_name = SomeType)]
+    #[wasm_bindgen(thread_local, js_name = SomeType)]
     static SOME_TYPE: JsValue;
 
     // Other bindings for SomeType
@@ -70,7 +71,7 @@ Strings can be imported to avoid going through `TextDecoder/Encoder` when requir
 ```rust
 #[wasm_bindgen]
 extern "C" {
-    #[wasm_bindgen(static_string)]
+    #[wasm_bindgen(thread_local, static_string)]
     static STRING: JsString = "a string literal";
 }
 ```
diff --git a/src/lib.rs b/src/lib.rs
index a6914269..e8e30b9a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1185,23 +1185,18 @@ impl Default for JsValue {
 /// This type implements `Deref` to the inner type so it's typically used as if
 /// it were `&T`.
 #[cfg(feature = "std")]
+#[deprecated = "use with `#[wasm_bindgen(thread_local)]` instead"]
 pub struct JsStatic<T: 'static> {
     #[doc(hidden)]
     pub __inner: &'static std::thread::LocalKey<T>,
 }
 
 #[cfg(feature = "std")]
+#[allow(deprecated)]
+#[cfg(not(target_feature = "atomics"))]
 impl<T: FromWasmAbi + 'static> Deref for JsStatic<T> {
     type Target = T;
     fn deref(&self) -> &T {
-        // We know that our tls key is never overwritten after initialization,
-        // so it should be safe (on that axis at least) to hand out a reference
-        // that lives longer than the closure below.
-        //
-        // FIXME: this is not sound if we ever implement thread exit hooks on
-        // wasm, as the pointer will eventually be invalidated but you can get
-        // `&'static T` from this interface. We... probably need to deprecate
-        // and/or remove this interface nowadays.
         unsafe { self.__inner.with(|ptr| &*(ptr as *const T)) }
     }
 }


Test patch:
diff --git a/crates/js-sys/tests/wasm/Function.rs b/crates/js-sys/tests/wasm/Function.rs
index c87a46cc..f0f27dca 100644
--- a/crates/js-sys/tests/wasm/Function.rs
+++ b/crates/js-sys/tests/wasm/Function.rs
@@ -5,13 +5,13 @@ use wasm_bindgen_test::*;
 
 #[wasm_bindgen]
 extern "C" {
-    #[wasm_bindgen(js_name = max, js_namespace = Math)]
+    #[wasm_bindgen(thread_local, js_name = max, js_namespace = Math)]
     static MAX: Function;
 
     type ArrayPrototype;
     #[wasm_bindgen(method, getter, structural)]
     pub fn push(this: &ArrayPrototype) -> Function;
-    #[wasm_bindgen(js_name = prototype, js_namespace = Array)]
+    #[wasm_bindgen(thread_local, js_name = prototype, js_namespace = Array)]
     static ARRAY_PROTOTYPE2: ArrayPrototype;
 }
 
@@ -21,12 +21,19 @@ fn apply() {
     args.push(&1.into());
     args.push(&2.into());
     args.push(&3.into());
-    assert_eq!(MAX.apply(&JsValue::undefined(), &args).unwrap(), 3);
+    assert_eq!(
+        MAX.with(|max| max.apply(&JsValue::undefined(), &args))
+            .unwrap(),
+        3
+    );
 
     let arr = JsValue::from(Array::new());
     let args = Array::new();
     args.push(&1.into());
-    ARRAY_PROTOTYPE2.push().apply(&arr, &args).unwrap();
+    ARRAY_PROTOTYPE2
+        .with(ArrayPrototype::push)
+        .apply(&arr, &args)
+        .unwrap();
     assert_eq!(Array::from(&arr).length(), 1);
 }
 
@@ -111,24 +118,29 @@ fn bind3() {
 
 #[wasm_bindgen_test]
 fn length() {
-    assert_eq!(MAX.length(), 2);
-    assert_eq!(ARRAY_PROTOTYPE2.push().length(), 1);
+    assert_eq!(MAX.with(Function::length), 2);
+    assert_eq!(ARRAY_PROTOTYPE2.with(ArrayPrototype::push).length(), 1);
 }
 
 #[wasm_bindgen_test]
 fn name() {
-    assert_eq!(JsValue::from(MAX.name()), "max");
-    assert_eq!(JsValue::from(ARRAY_PROTOTYPE2.push().name()), "push");
+    assert_eq!(JsValue::from(MAX.with(Function::name)), "max");
+    assert_eq!(
+        JsValue::from(ARRAY_PROTOTYPE2.with(ArrayPrototype::push).name()),
+        "push"
+    );
 }
 
 #[wasm_bindgen_test]
 fn to_string() {
-    assert!(MAX.to_string().length() > 0);
+    assert!(MAX.with(Function::to_string).length() > 0);
 }
 
 #[wasm_bindgen_test]
 fn function_inheritance() {
-    assert!(MAX.is_instance_of::<Function>());
-    assert!(MAX.is_instance_of::<Object>());
-    let _: &Object = MAX.as_ref();
+    assert!(MAX.with(Function::is_instance_of::<Function>));
+    assert!(MAX.with(Function::is_instance_of::<Object>));
+    MAX.with(|max| {
+        let _: &Object = max.as_ref();
+    });
 }
diff --git a/crates/js-sys/tests/wasm/Object.rs b/crates/js-sys/tests/wasm/Object.rs
index 69db1af3..b97c31d0 100644
--- a/crates/js-sys/tests/wasm/Object.rs
+++ b/crates/js-sys/tests/wasm/Object.rs
@@ -9,9 +9,9 @@ extern "C" {
     #[wasm_bindgen(method, setter, structural)]
     fn set_foo(this: &Foo42, val: JsValue);
 
-    #[wasm_bindgen(js_name = prototype, js_namespace = Object)]
+    #[wasm_bindgen(thread_local, js_name = prototype, js_namespace = Object)]
     static OBJECT_PROTOTYPE: JsValue;
-    #[wasm_bindgen(js_name = prototype, js_namespace = Array)]
+    #[wasm_bindgen(thread_local, js_name = prototype, js_namespace = Array)]
     static ARRAY_PROTOTYPE: JsValue;
 
     type DefinePropertyAttrs;
@@ -32,9 +32,9 @@ extern "C" {
     #[wasm_bindgen(constructor)]
     fn new() -> Foo;
 
-    #[wasm_bindgen(js_name = prototype, js_namespace = Foo)]
+    #[wasm_bindgen(thread_local, js_name = prototype, js_namespace = Foo)]
     static FOO_PROTOTYPE: Object;
-    #[wasm_bindgen(js_name = prototype, js_namespace = Bar)]
+    #[wasm_bindgen(thread_local, js_name = prototype, js_namespace = Bar)]
     static BAR_PROTOTYPE: Object;
 }
 
@@ -178,9 +178,9 @@ fn get_own_property_symbols() {
 #[wasm_bindgen_test]
 fn get_prototype_of() {
     let proto = JsValue::from(Object::get_prototype_of(&Object::new().into()));
-    assert_eq!(proto, *OBJECT_PROTOTYPE);
+    OBJECT_PROTOTYPE.with(|op| assert_eq!(proto, *op));
     let proto = JsValue::from(Object::get_prototype_of(&Array::new().into()));
-    assert_eq!(proto, *ARRAY_PROTOTYPE);
+    ARRAY_PROTOTYPE.with(|ap| assert_eq!(proto, *ap));
 }
 
 #[wasm_bindgen_test]
@@ -249,8 +249,8 @@ fn is_sealed() {
 #[wasm_bindgen_test]
 fn is_prototype_of() {
     let foo = JsValue::from(Foo::new());
-    assert!(FOO_PROTOTYPE.is_prototype_of(&foo));
-    assert!(!BAR_PROTOTYPE.is_prototype_of(&foo));
+    assert!(FOO_PROTOTYPE.with(|fp| fp.is_prototype_of(&foo)));
+    assert!(!BAR_PROTOTYPE.with(|bp| bp.is_prototype_of(&foo)));
 }
 
 #[wasm_bindgen_test]
diff --git a/crates/macro/ui-tests/invalid-items.rs b/crates/macro/ui-tests/invalid-items.rs
index f1a35633..9ab441c0 100644
--- a/crates/macro/ui-tests/invalid-items.rs
+++ b/crates/macro/ui-tests/invalid-items.rs
@@ -10,15 +10,26 @@ pub const fn foo2() {}
 struct Foo<T>(T);
 
 #[wasm_bindgen]
+#[rustfmt::skip]
 extern "C" {
     static mut FOO: u32;
 
+    #[wasm_bindgen(static_string)]
+    static FOO2: JsString;
+
+    #[wasm_bindgen(thread_local, static_string)]
+    static FOO3: JsString;
+
+    static FOO4: JsString = "test";
+
+    #[wasm_bindgen(static_string)]
+    static FOO5: JsString = "test";
+
     pub fn foo3(x: i32, ...);
 }
 
 #[wasm_bindgen]
-extern "system" {
-}
+extern "system" {}
 
 #[wasm_bindgen]
 pub fn foo4<T>() {}
diff --git a/crates/macro/ui-tests/invalid-items.stderr b/crates/macro/ui-tests/invalid-items.stderr
index 1800ade2..f985c96c 100644
--- a/crates/macro/ui-tests/invalid-items.stderr
+++ b/crates/macro/ui-tests/invalid-items.stderr
@@ -17,43 +17,67 @@ error: structs with #[wasm_bindgen] cannot have lifetime or type parameters curr
    |           ^^^
 
 error: cannot import mutable globals yet
-  --> $DIR/invalid-items.rs:14:12
+  --> $DIR/invalid-items.rs:15:12
    |
-14 |     static mut FOO: u32;
+15 |     static mut FOO: u32;
    |            ^^^
 
+error: static strings require a string literal
+  --> $DIR/invalid-items.rs:17:20
+   |
+17 |     #[wasm_bindgen(static_string)]
+   |                    ^^^^^^^^^^^^^
+
+error: static strings require a string literal
+  --> $DIR/invalid-items.rs:20:34
+   |
+20 |     #[wasm_bindgen(thread_local, static_string)]
+   |                                  ^^^^^^^^^^^^^
+
+error: static strings require `#[wasm_bindgen(static_string)]`
+  --> $DIR/invalid-items.rs:23:5
+   |
+23 |     static FOO4: JsString = "test";
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: static strings require `#[wasm_bindgen(thread_local)]`
+  --> $DIR/invalid-items.rs:26:5
+   |
+26 |     static FOO5: JsString = "test";
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
 error: can't #[wasm_bindgen] variadic functions
-  --> $DIR/invalid-items.rs:16:25
+  --> $DIR/invalid-items.rs:28:25
    |
-16 |     pub fn foo3(x: i32, ...);
+28 |     pub fn foo3(x: i32, ...);
    |                         ^^^
 
 error: only foreign mods with the `C` ABI are allowed
-  --> $DIR/invalid-items.rs:20:8
+  --> $DIR/invalid-items.rs:32:8
    |
-20 | extern "system" {
+32 | extern "system" {}
    |        ^^^^^^^^
 
 error: can't #[wasm_bindgen] functions with lifetime or type parameters
-  --> $DIR/invalid-items.rs:24:12
+  --> $DIR/invalid-items.rs:35:12
    |
-24 | pub fn foo4<T>() {}
+35 | pub fn foo4<T>() {}
    |            ^^^
 
 error: can't #[wasm_bindgen] functions with lifetime or type parameters
-  --> $DIR/invalid-items.rs:26:12
+  --> $DIR/invalid-items.rs:37:12
    |
-26 | pub fn foo5<'a>() {}
+37 | pub fn foo5<'a>() {}
    |            ^^^^
 
 error: can't #[wasm_bindgen] functions with lifetime or type parameters
-  --> $DIR/invalid-items.rs:28:12
+  --> $DIR/invalid-items.rs:39:12
    |
-28 | pub fn foo6<'a, T>() {}
+39 | pub fn foo6<'a, T>() {}
    |            ^^^^^^^
 
 error: #[wasm_bindgen] can only be applied to a function, struct, enum, impl, or extern block
-  --> $DIR/invalid-items.rs:31:1
+  --> $DIR/invalid-items.rs:42:1
    |
-31 | trait X {}
+42 | trait X {}
    | ^^^^^^^^^^
diff --git a/crates/macro/ui-tests/invalid-static-string.rs b/crates/macro/ui-tests/invalid-static-string.rs
new file mode 100644
index 00000000..7b50b107
--- /dev/null
+++ b/crates/macro/ui-tests/invalid-static-string.rs
@@ -0,0 +1,10 @@
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+#[rustfmt::skip]
+extern "C" {
+    #[wasm_bindgen(thread_local, static_string)]
+    static FOO: JsValue = "test";
+}
+
+fn main() {}
diff --git a/crates/macro/ui-tests/invalid-static-string.stderr b/crates/macro/ui-tests/invalid-static-string.stderr
new file mode 100644
index 00000000..7d1872a1
--- /dev/null
+++ b/crates/macro/ui-tests/invalid-static-string.stderr
@@ -0,0 +1,14 @@
+error[E0308]: mismatched types
+ --> $DIR/invalid-static-string.rs:3:1
+  |
+3 | #[wasm_bindgen]
+  | ^^^^^^^^^^^^^^^
+  | |
+  | expected `JsString`, found `JsValue`
+  | expected `JsString` because of return type
+  |
+  = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)
+help: call `Into::into` on this expression to convert `wasm_bindgen::JsValue` into `JsString`
+  |
+3 | #[wasm_bindgen].into()
+  |                +++++++
diff --git a/crates/test/src/rt/browser.rs b/crates/test/src/rt/browser.rs
index fc576f6d..49b27725 100644
--- a/crates/test/src/rt/browser.rs
+++ b/crates/test/src/rt/browser.rs
@@ -19,7 +19,8 @@ pub struct Browser {
 #[wasm_bindgen]
 extern "C" {
     type HTMLDocument;
-    static document: HTMLDocument;
+    #[wasm_bindgen(thread_local, js_name = document)]
+    static DOCUMENT: HTMLDocument;
     #[wasm_bindgen(method, structural)]
     fn getElementById(this: &HTMLDocument, id: &str) -> Element;
 
@@ -38,7 +39,7 @@ impl Browser {
     /// Creates a new instance of `Browser`, assuming that its APIs will work
     /// (requires `Node::new()` to have return `None` first).
     pub fn new() -> Browser {
-        let pre = document.getElementById("output");
+        let pre = DOCUMENT.with(|document| document.getElementById("output"));
         pre.set_text_content("");
         Browser { pre }
     }
diff --git a/crates/web-sys/tests/wasm/history.rs b/crates/web-sys/tests/wasm/history.rs
index f3ecdc42..e2800520 100644
--- a/crates/web-sys/tests/wasm/history.rs
+++ b/crates/web-sys/tests/wasm/history.rs
@@ -4,25 +4,27 @@ use web_sys::{History, ScrollRestoration};
 
 #[wasm_bindgen]
 extern "C" {
-    #[wasm_bindgen(js_name = history, js_namespace = window)]
+    #[wasm_bindgen(thread_local, js_name = history, js_namespace = window)]
     static HISTORY: History;
 }
 
 #[wasm_bindgen_test]
 fn history() {
-    HISTORY
-        .set_scroll_restoration(ScrollRestoration::Manual)
-        .expect("failure to set scroll restoration");
-    assert_eq!(
-        HISTORY.scroll_restoration().unwrap(),
-        ScrollRestoration::Manual
-    );
+    HISTORY.with(|history| {
+        history
+            .set_scroll_restoration(ScrollRestoration::Manual)
+            .expect("failure to set scroll restoration");
+        assert_eq!(
+            history.scroll_restoration().unwrap(),
+            ScrollRestoration::Manual
+        );
 
-    HISTORY
-        .set_scroll_restoration(ScrollRestoration::Auto)
-        .expect("failure to set scroll restoration");
-    assert_eq!(
-        HISTORY.scroll_restoration().unwrap(),
-        ScrollRestoration::Auto
-    );
+        history
+            .set_scroll_restoration(ScrollRestoration::Auto)
+            .expect("failure to set scroll restoration");
+        assert_eq!(
+            history.scroll_restoration().unwrap(),
+            ScrollRestoration::Auto
+        );
+    });
 }
diff --git a/crates/web-sys/tests/wasm/performance.rs b/crates/web-sys/tests/wasm/performance.rs
index 84931a62..09fb59a6 100644
--- a/crates/web-sys/tests/wasm/performance.rs
+++ b/crates/web-sys/tests/wasm/performance.rs
@@ -4,12 +4,12 @@ use web_sys::Performance;
 
 #[wasm_bindgen]
 extern "C" {
-    #[wasm_bindgen(js_name = performance)]
+    #[wasm_bindgen(thread_local, js_name = performance)]
     static PERFORMANCE: Performance;
 }
 
 #[wasm_bindgen_test]
 fn to_json() {
-    let perf = JsValue::from(PERFORMANCE.to_json());
+    let perf = JsValue::from(PERFORMANCE.with(Performance::to_json));
     assert!(perf.is_object());
 }
diff --git a/tests/headless/strings.rs b/tests/headless/strings.rs
index 944db5f2..baa551b8 100644
--- a/tests/headless/strings.rs
+++ b/tests/headless/strings.rs
@@ -15,7 +15,7 @@ fn string_roundtrip() {
 
     assert_eq!("\u{feff}bar", &identity("\u{feff}bar"));
 
-    assert_eq!(String::from(&*STRING), "foo")
+    assert_eq!(STRING.with(|s| String::from(s)), "foo");
 }
 
 #[wasm_bindgen]
@@ -23,6 +23,6 @@ fn string_roundtrip() {
 // See <https://github.com/rust-lang/rustfmt/issues/6267>.
 #[rustfmt::skip]
 extern "C" {
-    #[wasm_bindgen(static_string)]
+    #[wasm_bindgen(thread_local, static_string)]
     static STRING: JsString = "foo";
 }
diff --git a/tests/non_wasm.rs b/tests/non_wasm.rs
index dd770732..ca8acad3 100644
--- a/tests/non_wasm.rs
+++ b/tests/non_wasm.rs
@@ -31,13 +31,14 @@ pub fn foo(x: bool) {
 #[wasm_bindgen]
 extern "C" {
     fn some_import();
+    #[wasm_bindgen(thread_local)]
     static A: JsValue;
 }
 
 #[wasm_bindgen]
 pub fn bar(_: &str) -> JsValue {
     some_import();
-    A.clone()
+    A.with(JsValue::clone)
 }
 
 #[wasm_bindgen]
diff --git a/tests/wasm/duplicates.rs b/tests/wasm/duplicates.rs
index 206fede7..348f67b8 100644
--- a/tests/wasm/duplicates.rs
+++ b/tests/wasm/duplicates.rs
@@ -6,7 +6,8 @@ pub mod same_function_different_locations_a {
     #[wasm_bindgen(module = "tests/wasm/duplicates_a.js")]
     extern "C" {
         pub fn foo();
-        pub static bar: JsValue;
+        #[wasm_bindgen(thread_local, js_name = bar)]
+        pub static BAR: JsValue;
     }
 }
 
@@ -16,7 +17,8 @@ pub mod same_function_different_locations_b {
     #[wasm_bindgen(module = "tests/wasm/duplicates_a.js")]
     extern "C" {
         pub fn foo();
-        pub static bar: JsValue;
+        #[wasm_bindgen(thread_local, js_name = bar)]
+        pub static BAR: JsValue;
     }
 }
 
@@ -24,8 +26,8 @@ pub mod same_function_different_locations_b {
 fn same_function_different_locations() {
     same_function_different_locations_a::foo();
     same_function_different_locations_b::foo();
-    assert_eq!(*same_function_different_locations_a::bar, 3);
-    assert_eq!(*same_function_different_locations_a::bar, 3);
+    same_function_different_locations_a::BAR.with(|bar| assert_eq!(*bar, 3));
+    same_function_different_locations_a::BAR.with(|bar| assert_eq!(*bar, 3));
 }
 
 pub mod same_function_different_modules_a {
@@ -34,7 +36,8 @@ pub mod same_function_different_modules_a {
     #[wasm_bindgen(module = "tests/wasm/duplicates_b.js")]
     extern "C" {
         pub fn foo() -> bool;
-        pub static bar: JsValue;
+        #[wasm_bindgen(thread_local, js_name = bar)]
+        pub static BAR: JsValue;
     }
 }
 
@@ -44,7 +47,8 @@ pub mod same_function_different_modules_b {
     #[wasm_bindgen(module = "tests/wasm/duplicates_c.js")]
     extern "C" {
         pub fn foo() -> bool;
-        pub static bar: JsValue;
+        #[wasm_bindgen(thread_local, js_name = bar)]
+        pub static BAR: JsValue;
     }
 }
 
@@ -52,6 +56,6 @@ pub mod same_function_different_modules_b {
 fn same_function_different_modules() {
     assert!(same_function_different_modules_a::foo());
     assert!(!same_function_different_modules_b::foo());
-    assert_eq!(*same_function_different_modules_a::bar, 4);
-    assert_eq!(*same_function_different_modules_b::bar, 5);
+    same_function_different_modules_a::BAR.with(|bar| assert_eq!(*bar, 4));
+    same_function_different_modules_b::BAR.with(|bar| assert_eq!(*bar, 5));
 }
diff --git a/tests/wasm/imports.rs b/tests/wasm/imports.rs
index a9fbb33b..aa5a8b4f 100644
--- a/tests/wasm/imports.rs
+++ b/tests/wasm/imports.rs
@@ -23,6 +23,7 @@ extern "C" {
 
     fn assert_valid_error(val: JsValue);
 
+    #[wasm_bindgen(thread_local)]
     static IMPORT: JsValue;
 
     #[wasm_bindgen(js_name = return_three)]
@@ -35,7 +36,7 @@ extern "C" {
 
     #[allow(non_camel_case_types)]
     type bar;
-    #[wasm_bindgen(js_namespace = bar, js_name = foo)]
+    #[wasm_bindgen(thread_local, js_namespace = bar, js_name = foo)]
     static FOO: JsValue;
 
     fn take_custom_type(f: CustomType) -> CustomType;
@@ -46,7 +47,7 @@ extern "C" {
 
     #[wasm_bindgen(js_name = "baz$")]
     fn renamed_with_dollar_sign();
-    #[wasm_bindgen(js_name = "$foo")]
+    #[wasm_bindgen(thread_local, js_name = "$foo")]
     static RENAMED: JsValue;
 
     fn unused_import();
@@ -57,6 +58,7 @@ extern "C" {
     #[wasm_bindgen(static_method_of = StaticMethodCheck)]
     fn static_method_of_right_this();
 
+    #[wasm_bindgen(thread_local)]
     static STATIC_STRING: String;
 
     #[derive(Clone)]
@@ -170,7 +172,7 @@ fn free_imports() {
 
 #[wasm_bindgen_test]
 fn import_a_field() {
-    assert_eq!(IMPORT.as_f64(), Some(1.0));
+    assert_eq!(IMPORT.with(JsValue::as_f64), Some(1.0));
 }
 
 #[wasm_bindgen_test]
@@ -190,7 +192,7 @@ fn rust_keyword() {
 
 #[wasm_bindgen_test]
 fn rust_keyword2() {
-    assert_eq!(FOO.as_f64(), Some(3.0));
+    assert_eq!(FOO.with(JsValue::as_f64), Some(3.0));
 }
 
 #[wasm_bindgen_test]
@@ -222,7 +224,7 @@ fn rename_with_string() {
 
 #[wasm_bindgen_test]
 fn rename_static_with_string() {
-    assert_eq!(RENAMED.as_f64(), Some(1.0));
+    assert_eq!(RENAMED.with(JsValue::as_f64), Some(1.0));
 }
 
 #[wasm_bindgen_test]
@@ -285,7 +287,7 @@ fn undefined_function_is_ok() {
 
 #[wasm_bindgen_test]
 fn static_string_ok() {
-    assert_eq!(*STATIC_STRING, "x");
+    STATIC_STRING.with(|s| assert_eq!(*s, "x"));
 }
 
 #[wasm_bindgen_test]
diff --git a/tests/wasm/node.rs b/tests/wasm/node.rs
index 8f57e173..59f43eb7 100644
--- a/tests/wasm/node.rs
+++ b/tests/wasm/node.rs
@@ -4,6 +4,7 @@ use wasm_bindgen_test::*;
 #[wasm_bindgen(module = "tests/wasm/node.js")]
 extern "C" {
     fn test_works();
+    #[wasm_bindgen(thread_local)]
     static FOO: JsValue;
     fn hit();
 }
@@ -11,7 +12,7 @@ extern "C" {
 #[wasm_bindgen_test]
 fn works() {
     hit();
-    assert_eq!(FOO.as_f64(), Some(1.0));
+    assert_eq!(FOO.with(JsValue::as_f64), Some(1.0));
     test_works();
 }