diff --git a/CHANGELOG.md b/CHANGELOG.md index 014f3e6f8e0d..e9f9997dd695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,9 @@ * 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";`. + [#4055](https://github.com/rustwasm/wasm-bindgen/pull/4055) + ### Changed * Stabilize Web Share API. diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 91aa23040e61..3474d3a5a51f 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -29,6 +29,8 @@ pub struct Program { pub inline_js: Vec, /// Path to wasm_bindgen pub wasm_bindgen: Path, + /// Path to js_sys + pub js_sys: Path, /// Path to wasm_bindgen_futures pub wasm_bindgen_futures: Path, } @@ -44,6 +46,7 @@ impl Default for Program { typescript_custom_sections: Default::default(), inline_js: Default::default(), wasm_bindgen: syn::parse_quote! { wasm_bindgen }, + js_sys: syn::parse_quote! { js_sys }, wasm_bindgen_futures: syn::parse_quote! { wasm_bindgen_futures }, } } @@ -160,6 +163,8 @@ pub enum ImportKind { Function(ImportFunction), /// Importing a static value Static(ImportStatic), + /// Importing a static string + String(ImportString), /// Importing a type/class Type(ImportType), /// Importing a JS enum @@ -272,6 +277,26 @@ pub struct ImportStatic { pub wasm_bindgen: Path, } +/// The type of a static string being imported +#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] +#[derive(Clone)] +pub struct ImportString { + /// The visibility of this static string in Rust + pub vis: syn::Visibility, + /// The type specified by the user, which we only use to show an error if the wrong type is used. + pub ty: syn::Type, + /// The name of the shim function used to access this static + pub shim: Ident, + /// The name of this static on the Rust side + pub rust_name: Ident, + /// Path to wasm_bindgen + pub wasm_bindgen: Path, + /// Path to js_sys + pub js_sys: Path, + /// The string to export. + pub string: String, +} + /// The metadata for a type being imported #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] #[derive(Clone)] @@ -502,6 +527,7 @@ impl ImportKind { match *self { ImportKind::Function(_) => true, ImportKind::Static(_) => false, + ImportKind::String(_) => false, ImportKind::Type(_) => false, ImportKind::Enum(_) => false, } diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index e76718148afd..c826d67cb4d5 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -9,6 +9,7 @@ use quote::quote_spanned; use quote::{quote, ToTokens}; use std::collections::{HashMap, HashSet}; use std::sync::Mutex; +use syn::parse_quote; use syn::spanned::Spanned; use wasm_bindgen_shared as shared; @@ -846,6 +847,7 @@ impl TryToTokens for ast::ImportKind { match *self { ast::ImportKind::Function(ref f) => f.try_to_tokens(tokens)?, ast::ImportKind::Static(ref s) => s.to_tokens(tokens), + ast::ImportKind::String(ref s) => s.to_tokens(tokens), ast::ImportKind::Type(ref t) => t.to_tokens(tokens), ast::ImportKind::Enum(ref e) => e.to_tokens(tokens), } @@ -1477,6 +1479,7 @@ impl<'a> ToTokens for DescribeImport<'a> { let f = match *self.kind { ast::ImportKind::Function(ref f) => f, ast::ImportKind::Static(_) => return, + ast::ImportKind::String(_) => return, ast::ImportKind::Type(_) => return, ast::ImportKind::Enum(_) => return, }; @@ -1641,44 +1644,19 @@ impl ToTokens for ast::Enum { impl ToTokens for ast::ImportStatic { fn to_tokens(&self, into: &mut TokenStream) { - let name = &self.rust_name; let ty = &self.ty; - let shim_name = &self.shim; - let vis = &self.vis; - let wasm_bindgen = &self.wasm_bindgen; - - let abi_ret = quote! { - #wasm_bindgen::convert::WasmRet<<#ty as #wasm_bindgen::convert::FromWasmAbi>::Abi> - }; - (quote! { - #[automatically_derived] - #vis static #name: #wasm_bindgen::JsStatic<#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; - } - - #[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, - } - }; - }) + static_import( + &self.vis, + &self.rust_name, + &self.wasm_bindgen, + ty, + ty, + &self.shim, + ) .to_tokens(into); Descriptor { - ident: shim_name, + ident: &self.shim, inner: quote! { <#ty as WasmDescribe>::describe(); }, @@ -1689,6 +1667,61 @@ impl ToTokens for ast::ImportStatic { } } +impl ToTokens for ast::ImportString { + fn to_tokens(&self, into: &mut TokenStream) { + let js_sys = &self.js_sys; + let actual_ty: syn::Type = parse_quote!(#js_sys::JsString); + + static_import( + &self.vis, + &self.rust_name, + &self.wasm_bindgen, + &actual_ty, + &self.ty, + &self.shim, + ) + .to_tokens(into); + } +} + +fn static_import( + vis: &syn::Visibility, + name: &Ident, + wasm_bindgen: &syn::Path, + actual_ty: &syn::Type, + 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; + } + + #[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, + } + }; + } +} + /// Emits the necessary glue tokens for "descriptor", generating an appropriate /// symbol name as well as attributes around the descriptor function itself. struct Descriptor<'a, T> { diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs index b571d046dfe0..0daa9ff1123b 100644 --- a/crates/backend/src/encode.rs +++ b/crates/backend/src/encode.rs @@ -297,6 +297,7 @@ fn shared_import_kind<'a>( Ok(match i { ast::ImportKind::Function(f) => ImportKind::Function(shared_import_function(f, intern)?), ast::ImportKind::Static(f) => ImportKind::Static(shared_import_static(f, intern)), + ast::ImportKind::String(f) => ImportKind::String(shared_import_string(f, intern)), ast::ImportKind::Type(f) => ImportKind::Type(shared_import_type(f, intern)), ast::ImportKind::Enum(f) => ImportKind::Enum(shared_import_enum(f, intern)), }) @@ -332,6 +333,13 @@ fn shared_import_static<'a>(i: &'a ast::ImportStatic, intern: &'a Interner) -> I } } +fn shared_import_string<'a>(i: &'a ast::ImportString, intern: &'a Interner) -> ImportString<'a> { + ImportString { + shim: intern.intern(&i.shim), + string: &i.string, + } +} + fn shared_import_type<'a>(i: &'a ast::ImportType, intern: &'a Interner) -> ImportType<'a> { ImportType { name: &i.js_name, diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 759ddcc46781..c95a2ae78929 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -3067,6 +3067,19 @@ impl<'a> Context<'a> { self.import_name(js) } + AuxImport::String(string) => { + assert!(kind == AdapterJsImportKind::Normal); + assert!(!variadic); + assert_eq!(args.len(), 0); + + let mut escaped = String::with_capacity(string.len()); + string.chars().for_each(|c| match c { + '`' | '\\' | '$' => escaped.extend(['\\', c]), + _ => escaped.extend([c]), + }); + Ok(format!("`{escaped}`")) + } + AuxImport::Closure { dtor, mutable, diff --git a/crates/cli-support/src/wit/mod.rs b/crates/cli-support/src/wit/mod.rs index 0053e351336c..f85537b8fa22 100644 --- a/crates/cli-support/src/wit/mod.rs +++ b/crates/cli-support/src/wit/mod.rs @@ -567,6 +567,7 @@ impl<'a> Context<'a> { match &import.kind { decode::ImportKind::Function(f) => self.import_function(&import, f), decode::ImportKind::Static(s) => self.import_static(&import, s), + decode::ImportKind::String(s) => self.import_string(s), decode::ImportKind::Type(t) => self.import_type(&import, t), decode::ImportKind::Enum(_) => Ok(()), } @@ -803,6 +804,32 @@ impl<'a> Context<'a> { Ok(()) } + fn import_string(&mut self, string: &decode::ImportString<'_>) -> Result<(), Error> { + let (import_id, _id) = match self.function_imports.get(string.shim) { + Some(pair) => *pair, + None => return Ok(()), + }; + + // Register the signature of this imported shim + let id = self.import_adapter( + import_id, + Function { + arguments: Vec::new(), + shim_idx: 0, + ret: Descriptor::Externref, + inner_ret: None, + }, + AdapterJsImportKind::Normal, + )?; + + // And then save off that this function is is an instanceof shim for an + // imported item. + self.aux + .import_map + .insert(id, AuxImport::String(string.string.to_owned())); + Ok(()) + } + fn import_type( &mut self, import: &decode::Import<'_>, diff --git a/crates/cli-support/src/wit/nonstandard.rs b/crates/cli-support/src/wit/nonstandard.rs index 70bca5ad9157..4a9d5598ff56 100644 --- a/crates/cli-support/src/wit/nonstandard.rs +++ b/crates/cli-support/src/wit/nonstandard.rs @@ -220,6 +220,9 @@ pub enum AuxImport { /// `JsImport`. Static(JsImport), + /// This import is expected to be a shim that returns an exported `JsString`. + String(String), + /// This import is intended to manufacture a JS closure with the given /// signature and then return that back to Rust. Closure { diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index 376c15b1133e..3e3f3113a0e1 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -87,10 +87,12 @@ macro_rules! attrgen { (main, Main(Span)), (start, Start(Span)), (wasm_bindgen, WasmBindgen(Span, syn::Path)), + (js_sys, JsSys(Span, syn::Path)), (wasm_bindgen_futures, WasmBindgenFutures(Span, syn::Path)), (skip, Skip(Span)), (typescript_type, TypeScriptType(Span, String, Span)), (getter_with_clone, GetterWithClone(Span)), + (static_string, StaticString(Span)), // For testing purposes only. (assert_no_shim, AssertNoShim(Span)), @@ -774,6 +776,57 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option)> + for syn::ItemStatic +{ + type Target = ast::ImportKind; + + fn convert( + self, + (program, opts, module): (&ast::Program, BindgenAttrs, &'a Option), + ) -> Result { + if let syn::StaticMutability::Mut(_) = self.mutability { + bail_span!(self.mutability, "cannot import mutable globals yet") + } + + let string = if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(string), + .. + }) = *self.expr.clone() + { + string.value() + } else { + bail_span!( + self.expr, + "statics with a value can only be string literals" + ) + }; + + if opts.static_string().is_none() { + bail_span!( + self, + "statics strings require `#[wasm_bindgen(static_string)]`" + ) + } + + let shim = format!( + "__wbg_string_{}_{}", + self.ident, + ShortHash((&module, &self.ident)), + ); + opts.check_used(); + Ok(ast::ImportKind::String(ast::ImportString { + ty: *self.ty, + vis: self.vis, + rust_name: self.ident.clone(), + shim: Ident::new(&shim, Span::call_site()), + wasm_bindgen: program.wasm_bindgen.clone(), + js_sys: program.js_sys.clone(), + string, + })) + } +} + impl ConvertToAst for syn::ItemFn { type Target = ast::Function; @@ -966,6 +1019,9 @@ impl<'a> MacroParse<(Option, &'a mut TokenStream)> for syn::Item { if let Some(path) = opts.wasm_bindgen() { program.wasm_bindgen = path.clone(); } + if let Some(path) = opts.js_sys() { + program.js_sys = path.clone(); + } if let Some(path) = opts.wasm_bindgen_futures() { program.wasm_bindgen_futures = path.clone(); } @@ -1466,6 +1522,20 @@ impl MacroParse for syn::ForeignItem { syn::ForeignItem::Fn(ref mut f) => &mut f.attrs, syn::ForeignItem::Type(ref mut t) => &mut t.attrs, syn::ForeignItem::Static(ref mut s) => &mut s.attrs, + syn::ForeignItem::Verbatim(v) => { + let mut item: syn::ItemStatic = + syn::parse(v.into()).expect("only foreign functions/types allowed for now"); + let item_opts = BindgenAttrs::find(&mut item.attrs)?; + let kind = item.convert((program, item_opts, &ctx.module))?; + + program.imports.push(ast::Import { + module: None, + js_namespace: None, + kind, + }); + + return Ok(()); + } _ => panic!("only foreign functions/types allowed for now"), }; BindgenAttrs::find(attrs)? @@ -1502,6 +1572,10 @@ pub fn module_from_opts( program.wasm_bindgen = path.clone(); } + if let Some(path) = opts.js_sys() { + program.js_sys = path.clone(); + } + if let Some(path) = opts.wasm_bindgen_futures() { program.wasm_bindgen_futures = path.clone(); } diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 0c3af022dbf4..e85ddf9b87ca 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -49,6 +49,7 @@ macro_rules! shared_api { enum ImportKind<'a> { Function(ImportFunction<'a>), Static(ImportStatic<'a>), + String(ImportString<'a>), Type(ImportType<'a>), Enum(StringEnum), } @@ -92,6 +93,11 @@ macro_rules! shared_api { shim: &'a str, } + struct ImportString<'a> { + shim: &'a str, + string: &'a str, + } + struct ImportType<'a> { name: &'a str, instanceof_shim: &'a str, diff --git a/guide/src/reference/static-js-objects.md b/guide/src/reference/static-js-objects.md index f4826ea0dd11..9c15fcbb3d5b 100644 --- a/guide/src/reference/static-js-objects.md +++ b/guide/src/reference/static-js-objects.md @@ -62,3 +62,15 @@ extern "C" { fn new() -> SomeType; } ``` + +## 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. + +```rust +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(static_string)] + static STRING: JsString = "a string literal"; +} +``` diff --git a/tests/headless/strings.rs b/tests/headless/strings.rs index 300187b2c916..944db5f2e565 100644 --- a/tests/headless/strings.rs +++ b/tests/headless/strings.rs @@ -1,3 +1,4 @@ +use js_sys::JsString; use wasm_bindgen::prelude::*; use wasm_bindgen_test::*; @@ -13,4 +14,15 @@ fn string_roundtrip() { test_string_roundtrip(&Closure::wrap(Box::new(|s| s))); assert_eq!("\u{feff}bar", &identity("\u{feff}bar")); + + assert_eq!(String::from(&*STRING), "foo") +} + +#[wasm_bindgen] +// Currently Rustfmt simply removes the value on this static. +// See . +#[rustfmt::skip] +extern "C" { + #[wasm_bindgen(static_string)] + static STRING: JsString = "foo"; }