Skip to content

Commit dcd8b15

Browse files
feat: rework public api, add missing export apis
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 87499c6 commit dcd8b15

38 files changed

Lines changed: 1292 additions & 186 deletions

‎CHANGELOG.md‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- Support for the custom memory page sizes proposal ([#22](https://github.com/explodingcamera/tinywasm/pull/22) by [@danielstuart14](https://github.com/danielstuart14))
12+
- Support for the `custom_page_sizes` proposal ([#22](https://github.com/explodingcamera/tinywasm/pull/22) by [@danielstuart14](https://github.com/danielstuart14))
1313
- Support for the `tail_call` proposal
1414
- Support for the `memory64` proposal
15-
- Support for the fixed-width `simd` proposal
15+
- Support for the `simd` proposal
1616
- Support for the `relaxed_simd` proposal
1717
- Support for the `wide_arithmetic` proposal
1818
- New `Engine` API (`tinywasm::Engine` and `engine::Config`) for runtime configuration
1919
- Resumable function execution with fuel/time-budget APIs (`call_resumable`, `resume_with_fuel`, `resume_with_time_budget`, `ExecProgress`)
2020
- Host-function fuel APIs: `FuncContext::charge_fuel` and `FuncContext::remaining_fuel`
2121
- `engine::FuelPolicy` and `engine::Config::fuel_policy` for fuel accounting behavior
2222
- New `canonicalize_nans` feature flag to enable canonicalizing NaN values in the `f32`, `f64`, and `v128` types
23+
- Public API rework for runtime object access:
24+
- export lookups: `func`, `func_typed`, `memory`, `memory_mut`
25+
- table/global access: `table`, `table_mut`, `global`, `global_mut`, `global_get`, `global_set`
26+
- generic export access: `extern_item`, `extern_item_mut`
27+
- export iteration: `ModuleInstance::exports`
28+
- module descriptors: `Module::imports`, `Module::exports`
29+
- raw memory data access: `MemoryRef::data`/`data_size`, `MemoryRefMut::data`/`data_mut`/`data_size`
2330

2431
### Changed
2532

2633
- Locals are now stored in the typed value stacks instead of a separate locals structure
2734
- Structured control flow is fully lowered to jump-oriented internal instructions during parsing
28-
- Stack and call-stack limits are configured via `engine::Config`
35+
- Stack and call-stack limits can now be configured via `engine::Config`
36+
- Module-internal by-index inspection APIs are now gated behind the `guest_debug` feature
2937

3038
### Breaking Changes
3139

@@ -41,12 +49,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4149
- Cargo feature `logging` was renamed to `log`
4250
- Increased MSRV to 1.90
4351
- `Error::ParseError` was renamed to `Error::Parser`, and `Error::Twasm` was added
52+
- `ModuleInstance` export lookup APIs were renamed:
53+
- `exported_func_untyped` -> `func`
54+
- `exported_func` -> `func_typed`
55+
- `exported_memory` -> `memory`
56+
- `exported_memory_mut` -> `memory_mut`
57+
- `Imports::link_module` now takes a `ModuleInstance` instead of a raw module instance id
58+
- `func_typed` now validates the exact wasm signature at lookup time and fails immediately on mismatches
4459

4560
### Fixed
4661

4762
- Fixed archive **no_std** support which was broken in the previous release, and added more tests to ensure it stays working
4863
- `ModuleInstance::exported_memory` and `FuncContext::exported_memory` are now actually immutable ([#41](https://github.com/explodingcamera/tinywasm/pull/41))
4964
- Check returns in untyped host functions ([#27](https://github.com/explodingcamera/tinywasm/pull/27)) (thanks [@WhaleKit](https://github.com/WhaleKit))
65+
- `MemoryRefMut::copy_within(src, dst, len)` now follows its documented argument order
66+
- Imported tables created with `Extern::table(ty, init)` now honor the provided init value
5067

5168
## [0.8.0] - 2024-08-29
5269

‎crates/cli/src/bin.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ fn run(module: Module, func: Option<String>, args: &[WasmValue]) -> Result<()> {
109109
let instance = module.instantiate(&mut store, None)?;
110110

111111
if let Some(func) = func {
112-
let func = instance.exported_func_untyped(&store, &func)?;
112+
let func = instance.func(&store, &func)?;
113113
let res = func.call(&mut store, args)?;
114114
info!("{res:?}");
115115
}

‎crates/parser/src/visit.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ impl<'a, R: WasmModuleResources> wasmparser::VisitOperator<'a> for FunctionBuild
200200
visit_ref_func(RefFunc, u32), visit_table_fill(TableFill, u32), visit_table_get(TableGet, u32), visit_table_set(TableSet, u32), visit_table_grow(TableGrow, u32), visit_table_size(TableSize, u32),
201201

202202
// Bulk Memory
203-
visit_memory_init(MemoryInit, u32, u32), visit_memory_fill(MemoryFill, u32), visit_memory_copy(MemoryCopy, u32, u32), visit_table_init(TableInit, u32, u32), visit_data_drop(DataDrop, u32), visit_elem_drop(ElemDrop, u32),
203+
visit_memory_init(MemoryInit, u32, u32), visit_memory_fill(MemoryFill, u32), visit_table_init(TableInit, u32, u32), visit_data_drop(DataDrop, u32), visit_elem_drop(ElemDrop, u32),
204204

205205
// Wide Arithmetic
206206
visit_i64_add128(I64Add128), visit_i64_sub128(I64Sub128), visit_i64_mul_wide_s(I64MulWideS), visit_i64_mul_wide_u(I64MulWideU)
@@ -440,7 +440,11 @@ impl<'a, R: WasmModuleResources> wasmparser::VisitOperator<'a> for FunctionBuild
440440
}
441441

442442
fn visit_table_copy(&mut self, dst_table: u32, src_table: u32) -> Self::Output {
443-
self.instructions.push(Instruction::TableCopy { from: src_table, to: dst_table });
443+
self.instructions.push(Instruction::TableCopy { dst_table, src_table });
444+
}
445+
446+
fn visit_memory_copy(&mut self, dst_mem: u32, src_mem: u32) -> Self::Output {
447+
self.instructions.push(Instruction::MemoryCopy { dst_mem, src_mem });
444448
}
445449

446450
// Reference Types

‎crates/tinywasm/Cargo.toml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ readme="../../README.md"
1515
name="tinywasm"
1616
path="src/lib.rs"
1717

18+
[package.metadata.docs.rs]
19+
features=["std", "parser", "archive", "log", "canonicalize_nans", "debug", "guest_debug"]
20+
rustdoc-args=["--cfg", "docsrs"]
21+
1822
[dependencies]
1923
log={workspace=true, optional=true}
2024
tinywasm-parser={version="0.9.0-alpha.0", path="../parser", default-features=false, optional=true}
@@ -51,6 +55,9 @@ canonicalize_nans=[]
5155
# derive Debug for runtime/types structs
5256
debug=["tinywasm-types/debug"]
5357

58+
# expose module-internal by-index inspection APIs
59+
guest_debug=[]
60+
5461
# enable x86-specific SIMD intrinsics in Value128 (uses unsafe code)
5562
# note: for x86 backend selection, compile with x86-64-v3 target features
5663
# (for example: `RUSTFLAGS="-C target-cpu=x86-64-v3"`)

‎crates/tinywasm/benches/argon2id.rs‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,22 @@ fn argon2id_from_twasm(twasm: &[u8]) -> Result<TinyWasmModule> {
2424
fn argon2id_run(module: TinyWasmModule) -> Result<()> {
2525
let mut store = Store::default();
2626
let instance = ModuleInstance::instantiate(&mut store, module.into(), None)?;
27-
let argon2 = instance.exported_func::<(i32, i32, i32), i32>(&store, "argon2id")?;
27+
let argon2 = instance.func_typed::<(i32, i32, i32), i32>(&store, "argon2id")?;
2828
argon2.call(&mut store, (1000, 2, 1))?;
2929
Ok(())
3030
}
3131

3232
fn criterion_benchmark(c: &mut Criterion) {
3333
let module = argon2id_parse().expect("argon2id_parse");
3434
let twasm = argon2id_to_twasm(&module).expect("argon2id_to_twasm");
35-
36-
c.bench_function("argon2id_parse", |b| b.iter(argon2id_parse));
37-
c.bench_function("argon2id_to_twasm", |b| b.iter(|| argon2id_to_twasm(&module)));
38-
c.bench_function("argon2id_from_twasm", |b| b.iter(|| argon2id_from_twasm(&twasm)));
39-
c.bench_function("argon2id", |b| b.iter(|| argon2id_run(module.clone())));
35+
let mut group = c.benchmark_group("argon2id");
36+
37+
group.measurement_time(std::time::Duration::from_secs(2));
38+
group.bench_function("argon2id_parse", |b| b.iter(argon2id_parse));
39+
group.bench_function("argon2id_to_twasm", |b| b.iter(|| argon2id_to_twasm(&module)));
40+
group.bench_function("argon2id_from_twasm", |b| b.iter(|| argon2id_from_twasm(&twasm)));
41+
group.measurement_time(std::time::Duration::from_secs(10));
42+
group.bench_function("argon2id", |b| b.iter(|| argon2id_run(module.clone())));
4043
}
4144

4245
criterion_group!(benches, criterion_benchmark);

‎crates/tinywasm/benches/fibonacci.rs‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ fn fibonacci_from_twasm(twasm: &[u8]) -> Result<TinyWasmModule> {
2323
fn fibonacci_run(module: TinyWasmModule, recursive: bool, n: i32) -> Result<()> {
2424
let mut store = Store::default();
2525
let instance = ModuleInstance::instantiate(&mut store, module.into(), None)?;
26-
let argon2 = instance.exported_func::<i32, i32>(
26+
let argon2 = instance.func_typed::<i32, i32>(
2727
&store,
2828
match recursive {
2929
true => "fibonacci_recursive",
@@ -37,12 +37,15 @@ fn fibonacci_run(module: TinyWasmModule, recursive: bool, n: i32) -> Result<()>
3737
fn criterion_benchmark(c: &mut Criterion) {
3838
let module = fibonacci_parse().expect("fibonacci_parse");
3939
let twasm = fibonacci_to_twasm(&module).expect("fibonacci_to_twasm");
40-
41-
c.bench_function("fibonacci_parse", |b| b.iter(fibonacci_parse));
42-
c.bench_function("fibonacci_to_twasm", |b| b.iter(|| fibonacci_to_twasm(&module)));
43-
c.bench_function("fibonacci_from_twasm", |b| b.iter(|| fibonacci_from_twasm(&twasm)));
44-
c.bench_function("fibonacci_iterative_60", |b| b.iter(|| fibonacci_run(module.clone(), false, 60)));
45-
c.bench_function("fibonacci_recursive_26", |b| b.iter(|| fibonacci_run(module.clone(), true, 26)));
40+
let mut group = c.benchmark_group("fibonacci");
41+
42+
group.measurement_time(std::time::Duration::from_secs(2));
43+
group.bench_function("fibonacci_parse", |b| b.iter(fibonacci_parse));
44+
group.bench_function("fibonacci_to_twasm", |b| b.iter(|| fibonacci_to_twasm(&module)));
45+
group.bench_function("fibonacci_from_twasm", |b| b.iter(|| fibonacci_from_twasm(&twasm)));
46+
group.measurement_time(std::time::Duration::from_secs(10));
47+
group.bench_function("fibonacci_iterative_60", |b| b.iter(|| fibonacci_run(module.clone(), false, 60)));
48+
group.bench_function("fibonacci_recursive_26", |b| b.iter(|| fibonacci_run(module.clone(), true, 26)));
4649
}
4750

4851
criterion_group!(benches, criterion_benchmark);

‎crates/tinywasm/benches/tinywasm.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ fn tinywasm_run(module: TinyWasmModule) -> Result<()> {
2626
let mut imports = Imports::default();
2727
imports.define("env", "printi32", Extern::typed_func(|_: FuncContext<'_>, _: i32| Ok(()))).expect("define");
2828
let instance = ModuleInstance::instantiate(&mut store, module.into(), Some(imports)).expect("instantiate");
29-
let hello = instance.exported_func::<(), ()>(&store, "hello").expect("exported_func");
29+
let hello = instance.func_typed::<(), ()>(&store, "hello").expect("func_typed");
3030
hello.call(&mut store, ()).expect("call");
3131
Ok(())
3232
}
@@ -35,11 +35,12 @@ fn criterion_benchmark(c: &mut Criterion) {
3535
let module = tinywasm_parse().expect("tinywasm_parse");
3636
let twasm = tinywasm_to_twasm(&module).expect("tinywasm_to_twasm");
3737
let mut group = c.benchmark_group("tinywasm");
38-
group.measurement_time(std::time::Duration::from_secs(10));
3938

39+
group.measurement_time(std::time::Duration::from_secs(2));
4040
group.bench_function("tinywasm_parse", |b| b.iter(tinywasm_parse));
4141
group.bench_function("tinywasm_to_twasm", |b| b.iter(|| tinywasm_to_twasm(&module)));
4242
group.bench_function("tinywasm_from_twasm", |b| b.iter(|| tinywasm_from_twasm(&twasm)));
43+
group.measurement_time(std::time::Duration::from_secs(10));
4344
group.bench_function("tinywasm", |b| b.iter(|| tinywasm_run(module.clone())));
4445
}
4546

‎crates/tinywasm/benches/tinywasm_modes.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ fn setup_typed_func(module: TinyWasmModule, engine: Option<Engine>) -> Result<(S
2424
imports.define("env", "printi32", Extern::typed_func(|_: FuncContext<'_>, _: i32| Ok(())))?;
2525

2626
let instance = ModuleInstance::instantiate(&mut store, module.into(), Some(imports))?;
27-
let func = instance.exported_func::<(), ()>(&store, "hello")?;
27+
let func = instance.func_typed::<(), ()>(&store, "hello")?;
2828
Ok((store, func))
2929
}
3030

‎crates/tinywasm/src/func.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub(crate) struct ExecutionState {
2222
/// A function handle
2323
#[cfg_attr(feature = "debug", derive(Debug))]
2424
pub struct FuncHandle {
25+
pub(crate) store_id: usize,
2526
pub(crate) module_addr: ModuleInstanceAddr,
2627
pub(crate) addr: u32,
2728
pub(crate) ty: FuncType,
@@ -53,6 +54,10 @@ impl FuncHandle {
5354
/// See <https://webassembly.github.io/spec/core/exec/modules.html#invocation>
5455
#[inline]
5556
pub fn call(&self, store: &mut Store, params: &[WasmValue]) -> Result<Vec<WasmValue>> {
57+
if self.store_id != store.id() {
58+
return Err(Error::InvalidStore);
59+
}
60+
5661
validate_call_params(&self.ty, params)?;
5762

5863
let func_inst = store.state.get_func(self.addr);
@@ -86,6 +91,10 @@ impl FuncHandle {
8691
store: &'store mut Store,
8792
params: &[WasmValue],
8893
) -> Result<FuncExecution<'store>> {
94+
if self.store_id != store.id() {
95+
return Err(Error::InvalidStore);
96+
}
97+
8998
validate_call_params(&self.ty, params)?;
9099

91100
let func_inst = store.state.get_func(self.addr);

‎crates/tinywasm/src/imports.rs‎

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use alloc::vec::Vec;
66
use core::fmt::Debug;
77

88
use crate::func::{FromWasmValueTuple, IntoWasmValueTuple, ValTypesFromTuple};
9-
use crate::{LinkingError, MemoryRef, MemoryRefMut, Result, log};
9+
use crate::instance::{ExternItemRef, ExternItemRefMut};
10+
use crate::{GlobalRef, GlobalRefMut, LinkingError, MemoryRef, MemoryRefMut, Result, TableRef, TableRefMut, log};
1011
use tinywasm_types::*;
1112

1213
/// The internal representation of a function
@@ -74,14 +75,54 @@ impl FuncContext<'_> {
7475
})
7576
}
7677

77-
/// Get a reference to an exported memory
78-
pub fn exported_memory(&self, name: &str) -> Result<MemoryRef<'_>> {
79-
self.module().exported_memory(self.store, name)
78+
/// Get a reference to a memory export.
79+
pub fn memory(&self, name: &str) -> Result<MemoryRef<'_>> {
80+
self.module().memory(self.store, name)
8081
}
8182

82-
/// Get a mutable reference to an exported memory
83-
pub fn exported_memory_mut(&mut self, name: &str) -> Result<MemoryRefMut<'_>> {
84-
self.module().exported_memory_mut(self.store, name)
83+
/// Get a mutable reference to a memory export.
84+
pub fn memory_mut(&mut self, name: &str) -> Result<MemoryRefMut<'_>> {
85+
self.module().memory_mut(self.store, name)
86+
}
87+
88+
/// Get any exported extern value by name.
89+
pub fn extern_item(&self, name: &str) -> Result<ExternItemRef<'_>> {
90+
self.module().extern_item(self.store, name)
91+
}
92+
93+
/// Get any exported extern value by name with mutable access when applicable.
94+
pub fn extern_item_mut(&mut self, name: &str) -> Result<ExternItemRefMut<'_>> {
95+
self.module().extern_item_mut(self.store, name)
96+
}
97+
98+
/// Get a reference to a table export.
99+
pub fn table(&self, name: &str) -> Result<TableRef<'_>> {
100+
self.module().table(self.store, name)
101+
}
102+
103+
/// Get a mutable reference to a table export.
104+
pub fn table_mut(&mut self, name: &str) -> Result<TableRefMut<'_>> {
105+
self.module().table_mut(self.store, name)
106+
}
107+
108+
/// Get the value of a global export.
109+
pub fn global_get(&self, name: &str) -> Result<WasmValue> {
110+
self.module().global_get(self.store, name)
111+
}
112+
113+
/// Get a reference to a global export.
114+
pub fn global(&self, name: &str) -> Result<GlobalRef<'_>> {
115+
self.module().global(self.store, name)
116+
}
117+
118+
/// Get a mutable reference to a global export.
119+
pub fn global_mut(&mut self, name: &str) -> Result<GlobalRefMut<'_>> {
120+
self.module().global_mut(self.store, name)
121+
}
122+
123+
/// Set the value of a mutable global export.
124+
pub fn global_set(&mut self, name: &str, value: WasmValue) -> Result<()> {
125+
self.module().global_set(self.store, name, value)
85126
}
86127

87128
/// Charge additional fuel from the currently running resumable invocation.
@@ -230,8 +271,12 @@ impl From<&Import> for ExternName {
230271
/// ```rust
231272
/// # use log;
232273
/// # fn main() -> tinywasm::Result<()> {
233-
/// use tinywasm::{Imports, Extern};
274+
/// use tinywasm::{Extern, Imports, Module, Store};
234275
/// use tinywasm::types::{ValType, TableType, MemoryType, MemoryArch, WasmValue};
276+
/// # let wasm = wat::parse_str("(module)").expect("valid wat");
277+
/// # let module = Module::parse_bytes(&wasm)?;
278+
/// # let mut store = Store::default();
279+
/// # let my_other_instance = module.instantiate(&mut store, None)?;
235280
/// let mut imports = Imports::new();
236281
///
237282
/// // function args can be either a single
@@ -249,18 +294,16 @@ impl From<&Import> for ExternName {
249294
/// .define("my_module", "table", Extern::table(table_type, table_init))?
250295
/// .define("my_module", "memory", Extern::memory(MemoryType::new(MemoryArch::I32, 1, Some(2), None)))?
251296
/// .define("my_module", "global_i32", Extern::global(WasmValue::I32(666), false))?
252-
/// .link_module("my_other_module", 0)?;
297+
/// .link_module("my_other_module", my_other_instance)?;
253298
/// # Ok(())
254299
/// # }
255300
/// ```
256-
///
257-
/// Note that module instance addresses for [`Imports::link_module`] can be obtained from [`crate::ModuleInstance::id`].
258301
/// Now, the imports object can be passed to [`crate::ModuleInstance::instantiate`].
259302
#[derive(Default, Clone)]
260303
#[cfg_attr(feature = "debug", derive(Debug))]
261304
pub struct Imports {
262305
values: BTreeMap<ExternName, Extern>,
263-
modules: BTreeMap<String, ModuleInstanceAddr>,
306+
modules: BTreeMap<String, crate::ModuleInstance>,
264307
}
265308

266309
pub(crate) enum ResolvedExtern<S, V> {
@@ -297,8 +340,8 @@ impl Imports {
297340
/// Link a module
298341
///
299342
/// This will automatically link all imported values on instantiation
300-
pub fn link_module(&mut self, name: &str, addr: ModuleInstanceAddr) -> Result<&mut Self> {
301-
self.modules.insert(name.to_string(), addr);
343+
pub fn link_module(&mut self, name: &str, instance: crate::ModuleInstance) -> Result<&mut Self> {
344+
self.modules.insert(name.to_string(), instance);
302345
Ok(self)
303346
}
304347

@@ -312,17 +355,20 @@ impl Imports {
312355
&mut self,
313356
store: &mut crate::Store,
314357
import: &Import,
315-
) -> Option<ResolvedExtern<ExternVal, Extern>> {
358+
) -> Result<Option<ResolvedExtern<ExternVal, Extern>>> {
316359
let name = ExternName::from(import);
317360
if let Some(v) = self.values.get(&name) {
318-
return Some(ResolvedExtern::Extern(v.clone()));
361+
return Ok(Some(ResolvedExtern::Extern(v.clone())));
319362
}
320-
if let Some(addr) = self.modules.get(&name.module) {
321-
let instance = store.get_module_instance(*addr)?;
322-
return Some(ResolvedExtern::Store(instance.export_addr(&import.name)?));
363+
if let Some(instance) = self.modules.get(&name.module) {
364+
if instance.0.store_id != store.id() {
365+
return Err(crate::Error::InvalidStore);
366+
}
367+
368+
return Ok(instance.export_addr(&import.name).map(ResolvedExtern::Store));
323369
}
324370

325-
None
371+
Ok(None)
326372
}
327373

328374
#[cfg(not(feature = "debug"))]
@@ -392,16 +438,17 @@ impl Imports {
392438
let mut imports = ResolvedImports::new();
393439

394440
for import in &*module.0.imports {
395-
match self.take(store, import).ok_or_else(|| LinkingError::unknown_import(import))? {
441+
match self.take(store, import)?.ok_or_else(|| LinkingError::unknown_import(import))? {
396442
// A link to something that needs to be added to the store
397443
ResolvedExtern::Extern(ex) => match (ex, &import.kind) {
398444
(Extern::Global { ty, val }, ImportKind::Global(import_ty)) => {
399445
Self::compare_types(import, &ty, import_ty)?;
400446
imports.globals.push(store.add_global(ty, val.into(), idx)?);
401447
}
402-
(Extern::Table { ty, .. }, ImportKind::Table(import_ty)) => {
448+
(Extern::Table { ty, init }, ImportKind::Table(import_ty)) => {
403449
Self::compare_table_types(import, &ty, import_ty)?;
404-
imports.tables.push(store.add_table(ty, idx)?);
450+
Self::compare_types(import, &ty.element_type, &init.val_type())?;
451+
imports.tables.push(store.add_table(ty, init, idx)?);
405452
}
406453
(Extern::Memory { ty }, ImportKind::Memory(import_ty)) => {
407454
Self::compare_memory_types(import, &ty, import_ty, None)?;

0 commit comments

Comments
 (0)