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 validate modules while parsing. Enabled by default.
53    ///
54    /// Disable this only for trusted input. Parsing without validation may produce
55    /// a module that violates runtime assumptions.
56    pub validation: bool,
57    /// Whether to optimize local memory allocation by skipping allocation of unused local memories.
58    pub optimize_local_memory_allocation: bool,
59    /// Whether to run the peephole rewrite optimizer.
60    pub optimize_rewrite: bool,
61
62    #[cfg(parallel_parser)]
63    /// Number of threads to use for parallel parsing.
64    ///
65    /// Requires the `parallel` feature. Ignored when the feature is disabled.
66    ///
67    /// - `None`: auto-detect based on available parallelism
68    /// - `Some(1)`: force single-threaded
69    /// - `Some(n)`: use up to `n` workers
70    pub parser_threads: Option<usize>,
71}
72
73impl Default for ParserOptions {
74    fn default() -> Self {
75        Self {
76            validation: true,
77            optimize_local_memory_allocation: true,
78            optimize_rewrite: true,
79            #[cfg(parallel_parser)]
80            parser_threads: None,
81        }
82    }
83}
84
85impl ParserOptions {
86    /// Enable or disable WebAssembly validation.
87    ///
88    /// Disable this only for trusted input. Parsing without validation may produce
89    /// a module that violates runtime assumptions.
90    pub const fn with_validation(mut self, enabled: bool) -> Self {
91        self.validation = enabled;
92        self
93    }
94
95    /// Returns whether WebAssembly validation is enabled.
96    pub const fn validation(&self) -> bool {
97        self.validation
98    }
99
100    /// Enable or disable the optimization that skips allocating unused local memories.
101    pub const fn with_local_memory_allocation_optimization(mut self, enabled: bool) -> Self {
102        self.optimize_local_memory_allocation = enabled;
103        self
104    }
105
106    /// Returns whether unused local memory allocation optimization is enabled.
107    pub const fn optimize_local_memory_allocation(&self) -> bool {
108        self.optimize_local_memory_allocation
109    }
110
111    /// Enable or disable the peephole rewrite optimizer.
112    pub const fn with_rewrite_optimization(mut self, enabled: bool) -> Self {
113        self.optimize_rewrite = enabled;
114        self
115    }
116
117    /// Returns whether the peephole rewrite optimizer is enabled.
118    pub const fn optimize_rewrite(&self) -> bool {
119        self.optimize_rewrite
120    }
121
122    #[cfg(parallel_parser)]
123    /// Set the number of threads for parallel parsing.
124    ///
125    /// Requires the `parallel` feature to have any effect.
126    pub const fn with_parser_threads(mut self, threads: usize) -> Self {
127        self.parser_threads = Some(threads);
128        self
129    }
130
131    #[cfg(parallel_parser)]
132    /// Returns the configured parser thread count, or `None` for auto-detect.
133    pub const fn parser_threads(&self) -> Option<usize> {
134        self.parser_threads
135    }
136}
137
138/// A WebAssembly parser
139#[derive(Debug, Default)]
140pub struct Parser {
141    options: ParserOptions,
142}
143
144impl Parser {
145    /// Create a new parser instance
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    /// Create a new parser with explicit options.
151    pub fn with_options(options: ParserOptions) -> Self {
152        Self { options }
153    }
154
155    /// Read back parser options.
156    pub const fn options(&self) -> &ParserOptions {
157        &self.options
158    }
159
160    fn create_validator(_options: ParserOptions) -> Validator {
161        let features = WasmFeatures::CALL_INDIRECT_OVERLONG
162            | WasmFeatures::BULK_MEMORY_OPT
163            | WasmFeatures::RELAXED_SIMD
164            | WasmFeatures::GC_TYPES
165            | WasmFeatures::REFERENCE_TYPES
166            | WasmFeatures::MUTABLE_GLOBAL
167            | WasmFeatures::MULTI_VALUE
168            | WasmFeatures::FLOATS
169            | WasmFeatures::BULK_MEMORY
170            | WasmFeatures::SATURATING_FLOAT_TO_INT
171            | WasmFeatures::SIGN_EXTENSION
172            | WasmFeatures::EXTENDED_CONST
173            | WasmFeatures::FUNCTION_REFERENCES
174            | WasmFeatures::TAIL_CALL
175            | WasmFeatures::MULTI_MEMORY
176            | WasmFeatures::SIMD
177            | WasmFeatures::MEMORY64
178            | WasmFeatures::CUSTOM_PAGE_SIZES
179            | WasmFeatures::WIDE_ARITHMETIC;
180        Validator::new_with_features(features)
181    }
182
183    #[cfg(feature = "std")]
184    fn read_more(stream: &mut impl std::io::Read, buffer: &mut alloc::vec::Vec<u8>, hint: usize) -> Result<usize> {
185        let len = buffer.len();
186        buffer.resize(len + hint, 0);
187        let read_bytes = stream
188            .read(&mut buffer[len..])
189            .map_err(|e| ParseError::Other(alloc::format!("Error reading from stream: {e}")))?;
190        buffer.truncate(len + read_bytes);
191        Ok(read_bytes)
192    }
193
194    /// Parse a [`Module`] from bytes
195    pub fn parse_module_bytes(&self, wasm: impl AsRef<[u8]>) -> Result<Module> {
196        let wasm = wasm.as_ref();
197        let mut validator = self.options.validation().then(|| Self::create_validator(self.options.clone()));
198        let mut reader = ModuleReader::default();
199
200        for payload in wasmparser::Parser::new(0).parse_all(wasm) {
201            match payload? {
202                wasmparser::Payload::CodeSectionStart { count, range, size } => {
203                    reader.begin_code_section(count, range, size, validator.as_mut(), &self.options)?;
204                }
205                wasmparser::Payload::CodeSectionEntry(function) => {
206                    reader.process_borrowed_code_section_entry(function, validator.as_mut(), &self.options)?;
207                }
208                payload => reader.process_payload(payload, validator.as_mut())?,
209            }
210        }
211
212        if !reader.end_reached {
213            return Err(ParseError::EndNotReached);
214        }
215
216        reader.process_pending_functions(&self.options)?;
217        reader.into_module(&self.options)
218    }
219
220    #[cfg(feature = "std")]
221    /// Parse a [`Module`] from a file. Requires `std` feature.
222    pub fn parse_module_file(&self, path: impl AsRef<crate::std::path::Path> + Clone) -> Result<Module> {
223        let file = crate::std::fs::File::open(&path)
224            .map_err(|e| ParseError::Other(alloc::format!("Error opening file {:?}: {}", path.as_ref(), e)))?;
225        self.parse_module_stream(&mut crate::std::io::BufReader::new(file))
226    }
227
228    #[cfg(feature = "std")]
229    /// Parse a [`Module`] from a stream. Requires `std` feature.
230    pub fn parse_module_stream(&self, mut stream: impl std::io::Read) -> Result<Module> {
231        let mut validator = self.options.validation().then(|| Self::create_validator(self.options.clone()));
232        let mut reader = ModuleReader::default();
233        let mut buffer = alloc::vec::Vec::new();
234        let mut parser = wasmparser::Parser::new(0);
235        let mut eof = false;
236        let mut buffer_offset = 0;
237
238        loop {
239            match parser.parse(&buffer[buffer_offset..], eof)? {
240                wasmparser::Chunk::NeedMoreData(hint) => {
241                    if buffer_offset != 0 {
242                        buffer.copy_within(buffer_offset.., 0);
243                        buffer.truncate(buffer.len() - buffer_offset);
244                        buffer_offset = 0;
245                    }
246                    let read_bytes = Self::read_more(&mut stream, &mut buffer, hint as usize)?;
247                    eof = read_bytes == 0;
248                }
249                wasmparser::Chunk::Parsed { consumed, payload } => {
250                    #[cfg(parallel_parser)]
251                    let mut deferred_code_section = None;
252
253                    match payload {
254                        wasmparser::Payload::CodeSectionStart { count, range, size } => {
255                            let defer = reader.begin_code_section(
256                                count,
257                                range.clone(),
258                                size,
259                                validator.as_mut(),
260                                &self.options,
261                            )?;
262
263                            #[cfg(parallel_parser)]
264                            if defer {
265                                deferred_code_section = Some((count, range.end - size as usize, size as usize));
266                            }
267
268                            #[cfg(not(parallel_parser))]
269                            let _ = defer;
270                        }
271                        wasmparser::Payload::CodeSectionEntry(function) => {
272                            reader.process_inline_code_section_entry(function, validator.as_mut(), &self.options)?;
273                        }
274                        payload => {
275                            reader.process_payload(payload, validator.as_mut())?;
276                        }
277                    }
278                    buffer_offset += consumed;
279
280                    #[cfg(parallel_parser)]
281                    if let Some((count, body_offset, section_size)) = deferred_code_section {
282                        while buffer.len() - buffer_offset < section_size {
283                            let remaining = section_size - (buffer.len() - buffer_offset);
284                            let read_bytes = Self::read_more(&mut stream, &mut buffer, remaining)?;
285                            if read_bytes == 0 {
286                                return Err(ParseError::ParseError {
287                                    message: "unexpected end-of-file".into(),
288                                    offset: body_offset + buffer.len() - buffer_offset,
289                                });
290                            }
291                        }
292
293                        let section_end = buffer_offset + section_size;
294                        let section_bytes = alloc::sync::Arc::<[u8]>::from(buffer[buffer_offset..section_end].to_vec());
295                        reader.queue_owned_code_section(count, body_offset, section_bytes, validator.as_mut())?;
296                        parser.skip_section();
297                        buffer_offset = section_end;
298                        continue;
299                    }
300
301                    if reader.end_reached {
302                        if buffer_offset != buffer.len() {
303                            return Err(ParseError::Other("trailing bytes after end of module".into()));
304                        }
305
306                        if !eof {
307                            let read_bytes = Self::read_more(&mut stream, &mut buffer, 1)?;
308                            eof = read_bytes == 0;
309
310                            if !eof {
311                                return Err(ParseError::Other("trailing bytes after end of module".into()));
312                            }
313                        }
314                    }
315
316                    if reader.end_reached || eof {
317                        reader.process_pending_functions(&self.options)?;
318                        return reader.into_module(&self.options);
319                    }
320                }
321            };
322        }
323    }
324}
325
326impl TryFrom<ModuleReader<'_>> for Module {
327    type Error = ParseError;
328
329    fn try_from(reader: ModuleReader<'_>) -> Result<Self> {
330        reader.into_module(&ParserOptions::default())
331    }
332}
333
334/// Parse a module from bytes
335pub fn parse_bytes(wasm: &[u8]) -> Result<Module> {
336    Parser::new().parse_module_bytes(wasm)
337}
338
339#[cfg(feature = "std")]
340/// Parse a module from a file. Requires the `std` feature.
341pub fn parse_file(path: impl AsRef<crate::std::path::Path> + Clone) -> Result<Module> {
342    Parser::new().parse_module_file(path)
343}
344
345#[cfg(feature = "std")]
346/// Parse a module from a stream. Requires the `std` feature.
347pub fn parse_stream(stream: impl crate::std::io::Read) -> Result<Module> {
348    Parser::new().parse_module_stream(stream)
349}