Skip to main content

tinywasm_parser/
lib.rs

1#![no_std]
2#![doc(test(
3    no_crate_inject,
4    attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_assignments, unused_variables))
5))]
6#![warn(missing_docs, rust_2018_idioms, unreachable_pub)]
7#![forbid(unsafe_code)]
8//! See [`tinywasm`](https://docs.rs/tinywasm) for documentation.
9
10extern crate alloc;
11
12#[cfg(feature = "std")]
13extern crate std;
14
15// log for logging (optional).
16#[cfg(feature = "log")]
17#[allow(clippy::single_component_path_imports, unused_imports)]
18use log;
19
20// noop fallback if logging is disabled.
21#[cfg(not(feature = "log"))]
22#[allow(unused_imports, unused_macros)]
23pub(crate) mod log {
24    macro_rules! debug    ( ($($tt:tt)*) => {{}} );
25    macro_rules! info    ( ($($tt:tt)*) => {{}} );
26    macro_rules! error    ( ($($tt:tt)*) => {{}} );
27    pub(crate) use debug;
28    pub(crate) use error;
29    pub(crate) use info;
30}
31
32mod conversion;
33mod error;
34mod macros;
35mod module;
36mod optimize;
37mod visit;
38
39#[cfg(parallel_parser)]
40mod parallel;
41
42pub use error::*;
43use module::ModuleReader;
44use wasmparser::{Validator, WasmFeatures};
45
46pub use tinywasm_types::Module;
47
48/// Parser optimization and lowering options.
49#[non_exhaustive]
50#[derive(Debug, Clone)]
51pub struct ParserOptions {
52    /// Whether to optimize local memory allocation by skipping allocation of unused local memories.
53    pub optimize_local_memory_allocation: bool,
54    /// Whether to run the peephole rewrite optimizer.
55    pub optimize_rewrite: bool,
56    /// Whether to remove `Nop` and `MergeBarrier` instructions after rewriting.
57    pub optimize_remove_nop: bool,
58
59    #[cfg(parallel_parser)]
60    /// Number of threads to use for parallel parsing.
61    ///
62    /// Requires the `parallel` feature. Ignored when the feature is disabled.
63    ///
64    /// - `None`: auto-detect based on available parallelism
65    /// - `Some(1)`: force single-threaded
66    /// - `Some(n)`: use up to `n` workers
67    pub parser_threads: Option<usize>,
68}
69
70impl Default for ParserOptions {
71    fn default() -> Self {
72        Self {
73            optimize_local_memory_allocation: true,
74            optimize_rewrite: true,
75            optimize_remove_nop: true,
76            #[cfg(parallel_parser)]
77            parser_threads: None,
78        }
79    }
80}
81
82impl ParserOptions {
83    /// Enable or disable the optimization that skips allocating unused local memories.
84    pub const fn with_local_memory_allocation_optimization(mut self, enabled: bool) -> Self {
85        self.optimize_local_memory_allocation = enabled;
86        self
87    }
88
89    /// Returns whether unused local memory allocation optimization is enabled.
90    pub const fn optimize_local_memory_allocation(&self) -> bool {
91        self.optimize_local_memory_allocation
92    }
93
94    /// Enable or disable the peephole rewrite optimizer.
95    pub const fn with_rewrite_optimization(mut self, enabled: bool) -> Self {
96        self.optimize_rewrite = enabled;
97        self
98    }
99
100    /// Returns whether the peephole rewrite optimizer is enabled.
101    pub const fn optimize_rewrite(&self) -> bool {
102        self.optimize_rewrite
103    }
104
105    /// Enable or disable `Nop`/`MergeBarrier` removal after rewriting.
106    pub const fn with_nop_removal_optimization(mut self, enabled: bool) -> Self {
107        self.optimize_remove_nop = enabled;
108        self
109    }
110
111    /// Returns whether `Nop`/`MergeBarrier` removal is enabled.
112    pub const fn optimize_remove_nop(&self) -> bool {
113        self.optimize_remove_nop
114    }
115
116    #[cfg(parallel_parser)]
117    /// Set the number of threads for parallel parsing.
118    ///
119    /// Requires the `parallel` feature to have any effect.
120    pub const fn with_parser_threads(mut self, threads: usize) -> Self {
121        self.parser_threads = Some(threads);
122        self
123    }
124
125    #[cfg(parallel_parser)]
126    /// Returns the configured parser thread count, or `None` for auto-detect.
127    pub const fn parser_threads(&self) -> Option<usize> {
128        self.parser_threads
129    }
130}
131
132/// A WebAssembly parser
133#[derive(Debug, Default)]
134pub struct Parser {
135    options: ParserOptions,
136}
137
138impl Parser {
139    /// Create a new parser instance
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Create a new parser with explicit options.
145    pub fn with_options(options: ParserOptions) -> Self {
146        Self { options }
147    }
148
149    /// Read back parser options.
150    pub const fn options(&self) -> &ParserOptions {
151        &self.options
152    }
153
154    fn create_validator(_options: ParserOptions) -> Validator {
155        let features = WasmFeatures::CALL_INDIRECT_OVERLONG
156            | WasmFeatures::BULK_MEMORY_OPT
157            | WasmFeatures::RELAXED_SIMD
158            | WasmFeatures::GC_TYPES
159            | WasmFeatures::REFERENCE_TYPES
160            | WasmFeatures::MUTABLE_GLOBAL
161            | WasmFeatures::MULTI_VALUE
162            | WasmFeatures::FLOATS
163            | WasmFeatures::BULK_MEMORY
164            | WasmFeatures::SATURATING_FLOAT_TO_INT
165            | WasmFeatures::SIGN_EXTENSION
166            | WasmFeatures::EXTENDED_CONST
167            | WasmFeatures::FUNCTION_REFERENCES
168            | WasmFeatures::TAIL_CALL
169            | WasmFeatures::MULTI_MEMORY
170            | WasmFeatures::SIMD
171            | WasmFeatures::MEMORY64
172            | WasmFeatures::CUSTOM_PAGE_SIZES
173            | WasmFeatures::WIDE_ARITHMETIC;
174        Validator::new_with_features(features)
175    }
176
177    #[cfg(feature = "std")]
178    fn read_more(stream: &mut impl std::io::Read, buffer: &mut alloc::vec::Vec<u8>, hint: usize) -> Result<usize> {
179        let len = buffer.len();
180        buffer.extend((0..hint).map(|_| 0u8));
181        let read_bytes = stream
182            .read(&mut buffer[len..])
183            .map_err(|e| ParseError::Other(alloc::format!("Error reading from stream: {e}")))?;
184        buffer.truncate(len + read_bytes);
185        Ok(read_bytes)
186    }
187
188    /// Parse a [`Module`] from bytes
189    pub fn parse_module_bytes(&self, wasm: impl AsRef<[u8]>) -> Result<Module> {
190        let wasm = wasm.as_ref();
191        let mut validator = Self::create_validator(self.options.clone());
192        let mut reader = ModuleReader::default();
193
194        for payload in wasmparser::Parser::new(0).parse_all(wasm) {
195            match payload? {
196                wasmparser::Payload::CodeSectionStart { count, range, size } => {
197                    reader.begin_code_section(count, range, size, &mut validator, &self.options)?;
198                }
199                wasmparser::Payload::CodeSectionEntry(function) => {
200                    reader.process_borrowed_code_section_entry(function, &mut validator, &self.options)?;
201                }
202                payload => reader.process_payload(payload, &mut validator)?,
203            }
204        }
205
206        if !reader.end_reached {
207            return Err(ParseError::EndNotReached);
208        }
209
210        reader.process_pending_functions(&self.options)?;
211        reader.into_module(&self.options)
212    }
213
214    #[cfg(feature = "std")]
215    /// Parse a [`Module`] from a file. Requires `std` feature.
216    pub fn parse_module_file(&self, path: impl AsRef<crate::std::path::Path> + Clone) -> Result<Module> {
217        let file = crate::std::fs::File::open(&path)
218            .map_err(|e| ParseError::Other(alloc::format!("Error opening file {:?}: {}", path.as_ref(), e)))?;
219        self.parse_module_stream(&mut crate::std::io::BufReader::new(file))
220    }
221
222    #[cfg(feature = "std")]
223    /// Parse a [`Module`] from a stream. Requires `std` feature.
224    pub fn parse_module_stream(&self, mut stream: impl std::io::Read) -> Result<Module> {
225        let mut validator = Self::create_validator(self.options.clone());
226        let mut reader = ModuleReader::default();
227        let mut buffer = alloc::vec::Vec::new();
228        let mut parser = wasmparser::Parser::new(0);
229        let mut eof = false;
230
231        loop {
232            match parser.parse(&buffer, eof)? {
233                wasmparser::Chunk::NeedMoreData(hint) => {
234                    let read_bytes = Self::read_more(&mut stream, &mut buffer, hint as usize)?;
235                    eof = read_bytes == 0;
236                }
237                wasmparser::Chunk::Parsed { consumed, payload } => {
238                    #[cfg(parallel_parser)]
239                    let mut deferred_code_section = None;
240
241                    match payload {
242                        wasmparser::Payload::CodeSectionStart { count, range, size } => {
243                            let defer =
244                                reader.begin_code_section(count, range.clone(), size, &mut validator, &self.options)?;
245
246                            #[cfg(parallel_parser)]
247                            if defer {
248                                deferred_code_section = Some((count, range.end - size as usize, size as usize));
249                            }
250
251                            #[cfg(not(parallel_parser))]
252                            let _ = defer;
253
254                            buffer.drain(..consumed);
255                        }
256                        wasmparser::Payload::CodeSectionEntry(function) => {
257                            reader.process_inline_code_section_entry(function, &mut validator, &self.options)?;
258                            buffer.drain(..consumed);
259                        }
260                        payload => {
261                            reader.process_payload(payload, &mut validator)?;
262                            buffer.drain(..consumed);
263                        }
264                    }
265
266                    #[cfg(parallel_parser)]
267                    if let Some((count, body_offset, section_size)) = deferred_code_section {
268                        while buffer.len() < section_size {
269                            let remaining = section_size - buffer.len();
270                            let read_bytes = Self::read_more(&mut stream, &mut buffer, remaining)?;
271                            if read_bytes == 0 {
272                                return Err(ParseError::ParseError {
273                                    message: "unexpected end-of-file".into(),
274                                    offset: body_offset + buffer.len(),
275                                });
276                            }
277                        }
278
279                        let section_bytes = alloc::sync::Arc::<[u8]>::from(buffer[..section_size].to_vec());
280                        reader.queue_owned_code_section(count, body_offset, section_bytes, &mut validator)?;
281                        parser.skip_section();
282                        buffer.drain(..section_size);
283                        continue;
284                    }
285
286                    if eof || reader.end_reached {
287                        reader.process_pending_functions(&self.options)?;
288                        return reader.into_module(&self.options);
289                    }
290                }
291            };
292        }
293    }
294}
295
296impl TryFrom<ModuleReader<'_>> for Module {
297    type Error = ParseError;
298
299    fn try_from(reader: ModuleReader<'_>) -> Result<Self> {
300        reader.into_module(&ParserOptions::default())
301    }
302}
303
304/// Parse a module from bytes
305pub fn parse_bytes(wasm: &[u8]) -> Result<Module> {
306    let data = Parser::new().parse_module_bytes(wasm)?;
307    Ok(data)
308}
309
310#[cfg(feature = "std")]
311/// Parse a module from a file. Requires the `std` feature.
312pub fn parse_file(path: impl AsRef<crate::std::path::Path> + Clone) -> Result<Module> {
313    let data = Parser::new().parse_module_file(path)?;
314    Ok(data)
315}
316
317#[cfg(feature = "std")]
318/// Parse a module from a stream. Requires `parser` and `std` features.
319pub fn parse_stream(stream: impl crate::std::io::Read) -> Result<Module> {
320    let data = Parser::new().parse_module_stream(stream)?;
321    Ok(data)
322}