Skip to content

Commit 7d0b11c

Browse files
authored
Support expr when use typescript_custom_section attribute (#3901)
* feat: support expr when use `typescript_custom_section` attribute * test: update typescript-tests * chore: update "APPROVED_SCHEMA_FILE_HASH" of shared lib * chore: cargo fmt * Apply suggestions from code review include fix typo and adding whitespace to ensure consistent code style. Co-authored-by: Liam Murphy <[email protected]> * chore(backend): fix typo * chore(typescript-tests): rename custom_section_type to custom_section_type.d.ts * fix(backend/codegen): change method flat_slices to flat_byte_slices in order to avoid unsafe code * fix(backend/codegen): use dynamic wasm_bindgen path as import entry * chore(typescript-tests): ignore *.d.ts file when test * chore(shared/program): rename CustomSection to LitOrExpr * doc(shared/lib): add doc for program[typescript_custom_sections], explain why there are different types of LitOrExpr when encoding and decoding * chore(shared): update "APPROVED_SCHEMA_FILE_HASH" of shared lib * doc: add docs for method encode_u32_to_fixed_len_bytes * refactor(backend/encode): rename method shared_typescript_custom_section to shared_lit_or_expr * refactor(__rt): extract methods from nested mod directly into `__rt` * chore: cargo fmt * chore(__rt): remove unnecessary TODO * chore(changelog): update change log Support Expressions when using the `typescript_custom_section` attribute[#3901] * Update CHANGELOG.md
1 parent d25a68e commit 7d0b11c

File tree

14 files changed

+219
-40
lines changed

14 files changed

+219
-40
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
### Added
77

8+
* Added support for arbitrary expressions when using `#[wasm_bindgen(typescript_custom_section)]`.
9+
[#3901](https://github.com/rustwasm/wasm-bindgen/pull/3901)
10+
811
* Implement `From<NonNull<T>>` for `JsValue`.
912
[#3877](https://github.com/rustwasm/wasm-bindgen/pull/3877)
1013

crates/backend/src/ast.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub struct Program {
2424
/// rust structs
2525
pub structs: Vec<Struct>,
2626
/// custom typescript sections to be included in the definition file
27-
pub typescript_custom_sections: Vec<String>,
27+
pub typescript_custom_sections: Vec<LitOrExpr>,
2828
/// Inline JS snippets
2929
pub inline_js: Vec<String>,
3030
/// Path to wasm_bindgen
@@ -460,6 +460,16 @@ pub enum TypeLocation {
460460
ExportRet,
461461
}
462462

463+
/// An enum representing either a literal value (`Lit`) or an expression (`syn::Expr`).
464+
#[cfg_attr(feature = "extra-traits", derive(Debug))]
465+
#[derive(Clone)]
466+
pub enum LitOrExpr {
467+
/// Represents an expression that needs to be evaluated before it can be encoded
468+
Expr(syn::Expr),
469+
/// Represents a literal string that can be directly encoded.
470+
Lit(String),
471+
}
472+
463473
impl Export {
464474
/// Mangles a rust -> javascript export, so that the created Ident will be unique over function
465475
/// name and class name, if the function belongs to a javascript class.

crates/backend/src/codegen.rs

+57-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::ast;
22
use crate::encode;
3+
use crate::encode::EncodeChunk;
34
use crate::Diagnostic;
45
use once_cell::sync::Lazy;
56
use proc_macro2::{Ident, Literal, Span, TokenStream};
@@ -94,17 +95,51 @@ impl TryToTokens for ast::Program {
9495
shared::SCHEMA_VERSION,
9596
shared::version()
9697
);
98+
99+
let wasm_bindgen = &self.wasm_bindgen;
100+
97101
let encoded = encode::encode(self)?;
98-
let len = prefix_json.len() as u32;
99-
let bytes = [
100-
&len.to_le_bytes()[..],
101-
prefix_json.as_bytes(),
102-
&encoded.custom_section,
103-
]
104-
.concat();
105102

106-
let generated_static_length = bytes.len();
107-
let generated_static_value = syn::LitByteStr::new(&bytes, Span::call_site());
103+
let encoded_chunks: Vec<_> = encoded
104+
.custom_section
105+
.iter()
106+
.map(|chunk| match chunk {
107+
EncodeChunk::EncodedBuf(buf) => {
108+
let buf = syn::LitByteStr::new(buf.as_slice(), Span::call_site());
109+
quote!(#buf)
110+
}
111+
EncodeChunk::StrExpr(expr) => {
112+
// encode expr as str
113+
quote!({
114+
use #wasm_bindgen::__rt::{encode_u32_to_fixed_len_bytes};
115+
const _STR_EXPR: &str = #expr;
116+
const _STR_EXPR_BYTES: &[u8] = _STR_EXPR.as_bytes();
117+
const _STR_EXPR_BYTES_LEN: usize = _STR_EXPR_BYTES.len() + 5;
118+
const _ENCODED_BYTES: [u8; _STR_EXPR_BYTES_LEN] = flat_byte_slices([
119+
&encode_u32_to_fixed_len_bytes(_STR_EXPR_BYTES.len() as u32),
120+
_STR_EXPR_BYTES,
121+
]);
122+
&_ENCODED_BYTES
123+
})
124+
}
125+
})
126+
.collect();
127+
128+
let chunk_len = encoded_chunks.len();
129+
130+
// concatenate all encoded chunks and write the length in front of the chunk;
131+
let encode_bytes = quote!({
132+
const _CHUNK_SLICES: [&[u8]; #chunk_len] = [
133+
#(#encoded_chunks,)*
134+
];
135+
const _CHUNK_LEN: usize = flat_len(_CHUNK_SLICES);
136+
const _CHUNKS: [u8; _CHUNK_LEN] = flat_byte_slices(_CHUNK_SLICES);
137+
138+
const _LEN_BYTES: [u8; 4] = (_CHUNK_LEN as u32).to_le_bytes();
139+
const _ENCODED_BYTES_LEN: usize = _CHUNK_LEN + 4;
140+
const _ENCODED_BYTES: [u8; _ENCODED_BYTES_LEN] = flat_byte_slices([&_LEN_BYTES, &_CHUNKS]);
141+
&_ENCODED_BYTES
142+
});
108143

109144
// We already consumed the contents of included files when generating
110145
// the custom section, but we want to make sure that updates to the
@@ -119,15 +154,26 @@ impl TryToTokens for ast::Program {
119154
quote! { include_str!(#file) }
120155
});
121156

157+
let len = prefix_json.len() as u32;
158+
let prefix_json_bytes = [&len.to_le_bytes()[..], prefix_json.as_bytes()].concat();
159+
let prefix_json_bytes = syn::LitByteStr::new(&prefix_json_bytes, Span::call_site());
160+
122161
(quote! {
123162
#[cfg(target_arch = "wasm32")]
124163
#[automatically_derived]
125164
const _: () = {
165+
use #wasm_bindgen::__rt::{flat_len, flat_byte_slices};
166+
126167
static _INCLUDED_FILES: &[&str] = &[#(#file_dependencies),*];
127168

169+
const _ENCODED_BYTES: &[u8] = #encode_bytes;
170+
const _PREFIX_JSON_BYTES: &[u8] = #prefix_json_bytes;
171+
const _ENCODED_BYTES_LEN: usize = _ENCODED_BYTES.len();
172+
const _PREFIX_JSON_BYTES_LEN: usize = _PREFIX_JSON_BYTES.len();
173+
const _LEN: usize = _PREFIX_JSON_BYTES_LEN + _ENCODED_BYTES_LEN;
174+
128175
#[link_section = "__wasm_bindgen_unstable"]
129-
pub static _GENERATED: [u8; #generated_static_length] =
130-
*#generated_static_value;
176+
static _GENERATED: [u8; _LEN] = flat_byte_slices([_PREFIX_JSON_BYTES, _ENCODED_BYTES]);
131177
};
132178
})
133179
.to_tokens(tokens);

crates/backend/src/encode.rs

+49-11
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@ use std::path::PathBuf;
99
use crate::ast;
1010
use crate::Diagnostic;
1111

12+
#[derive(Clone)]
13+
pub enum EncodeChunk {
14+
EncodedBuf(Vec<u8>),
15+
StrExpr(syn::Expr),
16+
// TODO: support more expr type;
17+
}
18+
1219
pub struct EncodeResult {
13-
pub custom_section: Vec<u8>,
20+
pub custom_section: Vec<EncodeChunk>,
1421
pub included_files: Vec<PathBuf>,
1522
}
1623

@@ -144,7 +151,7 @@ fn shared_program<'a>(
144151
typescript_custom_sections: prog
145152
.typescript_custom_sections
146153
.iter()
147-
.map(|x| -> &'a str { x })
154+
.map(|x| shared_lit_or_expr(x, intern))
148155
.collect(),
149156
linked_modules: prog
150157
.linked_modules
@@ -253,6 +260,13 @@ fn shared_import<'a>(i: &'a ast::Import, intern: &'a Interner) -> Result<Import<
253260
})
254261
}
255262

263+
fn shared_lit_or_expr<'a>(i: &'a ast::LitOrExpr, _intern: &'a Interner) -> LitOrExpr<'a> {
264+
match i {
265+
ast::LitOrExpr::Lit(lit) => LitOrExpr::Lit(lit),
266+
ast::LitOrExpr::Expr(expr) => LitOrExpr::Expr(expr),
267+
}
268+
}
269+
256270
fn shared_linked_module<'a>(
257271
name: &str,
258272
i: &'a ast::ImportModule,
@@ -358,24 +372,48 @@ trait Encode {
358372
}
359373

360374
struct Encoder {
361-
dst: Vec<u8>,
375+
dst: Vec<EncodeChunk>,
376+
}
377+
378+
enum LitOrExpr<'a> {
379+
Expr(&'a syn::Expr),
380+
Lit(&'a str),
381+
}
382+
383+
impl<'a> Encode for LitOrExpr<'a> {
384+
fn encode(&self, dst: &mut Encoder) {
385+
match self {
386+
LitOrExpr::Expr(expr) => {
387+
dst.dst.push(EncodeChunk::StrExpr((*expr).clone()));
388+
}
389+
LitOrExpr::Lit(s) => s.encode(dst),
390+
}
391+
}
362392
}
363393

364394
impl Encoder {
365395
fn new() -> Encoder {
366-
Encoder {
367-
dst: vec![0, 0, 0, 0],
368-
}
396+
Encoder { dst: vec![] }
369397
}
370398

371-
fn finish(mut self) -> Vec<u8> {
372-
let len = (self.dst.len() - 4) as u32;
373-
self.dst[..4].copy_from_slice(&len.to_le_bytes()[..]);
399+
fn finish(self) -> Vec<EncodeChunk> {
374400
self.dst
375401
}
376402

377403
fn byte(&mut self, byte: u8) {
378-
self.dst.push(byte);
404+
if let Some(EncodeChunk::EncodedBuf(buf)) = self.dst.last_mut() {
405+
buf.push(byte);
406+
} else {
407+
self.dst.push(EncodeChunk::EncodedBuf(vec![byte]));
408+
}
409+
}
410+
411+
fn extend_from_slice(&mut self, slice: &[u8]) {
412+
if let Some(EncodeChunk::EncodedBuf(buf)) = self.dst.last_mut() {
413+
buf.extend_from_slice(slice);
414+
} else {
415+
self.dst.push(EncodeChunk::EncodedBuf(slice.to_owned()));
416+
}
379417
}
380418
}
381419

@@ -407,7 +445,7 @@ impl Encode for usize {
407445
impl<'a> Encode for &'a [u8] {
408446
fn encode(&self, dst: &mut Encoder) {
409447
self.len().encode(dst);
410-
dst.dst.extend_from_slice(self);
448+
dst.extend_from_slice(self);
411449
}
412450
}
413451

crates/cli-support/src/decode.rs

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::str;
1+
use std::{ops::Deref, str};
22

33
pub trait Decode<'src>: Sized {
44
fn decode(data: &mut &'src [u8]) -> Self;
@@ -10,12 +10,30 @@ pub trait Decode<'src>: Sized {
1010
}
1111
}
1212

13+
pub struct LitOrExpr<'src> {
14+
str: &'src str,
15+
}
16+
1317
fn get(b: &mut &[u8]) -> u8 {
1418
let r = b[0];
1519
*b = &b[1..];
1620
r
1721
}
1822

23+
impl<'src> Deref for LitOrExpr<'src> {
24+
type Target = str;
25+
fn deref(&self) -> &Self::Target {
26+
self.str
27+
}
28+
}
29+
30+
impl<'src> Decode<'src> for LitOrExpr<'src> {
31+
fn decode(data: &mut &'src [u8]) -> Self {
32+
let str = <&'src str>::decode(data);
33+
Self { str }
34+
}
35+
}
36+
1937
impl<'src> Decode<'src> for bool {
2038
fn decode(data: &mut &'src [u8]) -> Self {
2139
get(data) != 0

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ impl<'a> Context<'a> {
455455
self.struct_(struct_)?;
456456
}
457457
for section in typescript_custom_sections {
458-
self.aux.extra_typescript.push_str(section);
458+
self.aux.extra_typescript.push_str(&section);
459459
self.aux.extra_typescript.push_str("\n\n");
460460
}
461461
self.aux
@@ -1536,14 +1536,14 @@ version of wasm-bindgen that uses a different bindgen format than this binary:
15361536
this binary schema version: {my_version}
15371537
15381538
Currently the bindgen format is unstable enough that these two schema versions
1539-
must exactly match. You can accomplish this by either updating this binary or
1539+
must exactly match. You can accomplish this by either updating this binary or
15401540
the wasm-bindgen dependency in the Rust project.
15411541
15421542
You should be able to update the wasm-bindgen dependency with:
15431543
15441544
cargo update -p wasm-bindgen --precise {my_version}
15451545
1546-
don't forget to recompile your wasm file! Alternatively, you can update the
1546+
don't forget to recompile your wasm file! Alternatively, you can update the
15471547
binary with:
15481548
15491549
cargo install -f wasm-bindgen-cli --version {their_version}

crates/macro-support/src/parser.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -1404,17 +1404,17 @@ impl MacroParse<BindgenAttrs> for syn::ItemConst {
14041404
bail_span!(self, "#[wasm_bindgen] will not work on constants unless you are defining a #[wasm_bindgen(typescript_custom_section)].");
14051405
}
14061406

1407-
match get_expr(&self.expr) {
1407+
let typescript_custom_section = match get_expr(&self.expr) {
14081408
syn::Expr::Lit(syn::ExprLit {
14091409
lit: syn::Lit::Str(litstr),
14101410
..
1411-
}) => {
1412-
program.typescript_custom_sections.push(litstr.value());
1413-
}
1414-
expr => {
1415-
bail_span!(expr, "Expected a string literal to be used with #[wasm_bindgen(typescript_custom_section)].");
1416-
}
1417-
}
1411+
}) => ast::LitOrExpr::Lit(litstr.value()),
1412+
expr => ast::LitOrExpr::Expr(expr.clone()),
1413+
};
1414+
1415+
program
1416+
.typescript_custom_sections
1417+
.push(typescript_custom_section);
14181418

14191419
opts.check_used();
14201420

crates/shared/src/lib.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ macro_rules! shared_api {
1717
enums: Vec<Enum<'a>>,
1818
imports: Vec<Import<'a>>,
1919
structs: Vec<Struct<'a>>,
20-
typescript_custom_sections: Vec<&'a str>,
20+
// NOTE: Originally typescript_custom_sections are just some strings
21+
// But the expression type can only be parsed into a string during compilation
22+
// So when encoding, LitOrExpr contains two types, one is that expressions are parsed into strings during compilation, and the other is can be parsed directly.
23+
// When decoding, LitOrExpr can be decoded as a string.
24+
typescript_custom_sections: Vec<LitOrExpr<'a>>,
2125
local_modules: Vec<LocalModule<'a>>,
2226
inline_js: Vec<&'a str>,
2327
unique_crate_identifier: &'a str,

crates/shared/src/schema_hash_approval.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// If the schema in this library has changed then:
99
// 1. Bump the version in `crates/shared/Cargo.toml`
1010
// 2. Change the `SCHEMA_VERSION` in this library to this new Cargo.toml version
11-
const APPROVED_SCHEMA_FILE_HASH: &str = "11955579329744078753";
11+
const APPROVED_SCHEMA_FILE_HASH: &str = "10197913343580353876";
1212

1313
#[test]
1414
fn schema_version() {

crates/typescript-tests/jest.config.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module.exports = {
44
testEnvironment: 'node',
55
extensionsToTreatAsEsm: [".ts"],
66
verbose: true,
7-
testMatch: ['**/src/*.ts'],
7+
testMatch: ['**/src/*.ts', '!**/src/*.d.ts'],
88
// TODO: migrate all test files and remove this
99
testPathIgnorePatterns: [
1010
".*/src/custom_section.ts$",

crates/typescript-tests/src/custom_section.rs

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ const TS_INTERFACE_EXPORT: &'static str = r"
55
interface Height { height: number; }
66
";
77

8+
#[wasm_bindgen(typescript_custom_section)]
9+
const TS_INTERFACE_EXPORT1: &'static str = include_str!("./custom_section_types.d.ts");
10+
11+
const TS_INTERFACE_EXPORT2: &str = "interface Person2 { height: number; }";
12+
#[wasm_bindgen(typescript_custom_section)]
13+
const _: &str = TS_INTERFACE_EXPORT2;
14+
815
#[wasm_bindgen]
916
pub struct Person {
1017
pub height: u32,
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
import * as wbg from '../pkg/typescript_tests';
1+
import * as wbg from "../pkg/typescript_tests"
22

3-
const height: wbg.Height = new wbg.Person();
3+
const height: wbg.Height = new wbg.Person()
4+
5+
const height1: wbg.Person1 = new wbg.Person()
6+
7+
const height2: wbg.Person2 = new wbg.Person()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
interface Person1 {
2+
height: number
3+
}

0 commit comments

Comments
 (0)