Compare commits
9 Commits
master
...
cmichi-add
Author | SHA1 | Date |
---|---|---|
Michael Mueller | c96915b4df | |
Michael Mueller | b1bd7950d9 | |
Michael Mueller | 70a2e612bc | |
Michael Mueller | 81f34a6ab6 | |
Michael Mueller | 91684c25ba | |
Michael Mueller | d08a08de51 | |
Michael Mueller | f111950cbd | |
Michael Mueller | 99b0e03e4e | |
Michael Mueller | 7546d3026d |
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "wasmi"
|
name = "wasmi"
|
||||||
version = "0.4.5"
|
version = "0.4.6"
|
||||||
authors = ["Nikolay Volf <nikvolf@gmail.com>", "Svyatoslav Nikolsky <svyatonik@yandex.ru>", "Sergey Pepyakin <s.pepyakin@gmail.com>"]
|
authors = ["Nikolay Volf <nikvolf@gmail.com>", "Svyatoslav Nikolsky <svyatonik@yandex.ru>", "Sergey Pepyakin <s.pepyakin@gmail.com>"]
|
||||||
license = "MIT/Apache-2.0"
|
license = "MIT/Apache-2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -17,6 +17,7 @@ memory_units = "0.3.0"
|
||||||
libm = { version = "0.1.2", optional = true }
|
libm = { version = "0.1.2", optional = true }
|
||||||
num-rational = "0.2.2"
|
num-rational = "0.2.2"
|
||||||
num-traits = "0.2.8"
|
num-traits = "0.2.8"
|
||||||
|
atomic = { version = "0.4", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_matches = "1.1"
|
assert_matches = "1.1"
|
||||||
|
@ -24,6 +25,7 @@ rand = "0.4.2"
|
||||||
wabt = "0.6"
|
wabt = "0.6"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
threadsafe = ["atomic"]
|
||||||
default = ["std"]
|
default = ["std"]
|
||||||
# Disable for no_std support
|
# Disable for no_std support
|
||||||
std = [
|
std = [
|
||||||
|
|
14
README.md
14
README.md
|
@ -18,6 +18,7 @@ git clone https://github.com/paritytech/wasmi.git --recursive
|
||||||
cd wasmi
|
cd wasmi
|
||||||
cargo build
|
cargo build
|
||||||
cargo test
|
cargo test
|
||||||
|
cargo test --features threadsafe
|
||||||
```
|
```
|
||||||
|
|
||||||
# `no_std` support
|
# `no_std` support
|
||||||
|
@ -39,6 +40,19 @@ Also, code related to `std::error` is disabled.
|
||||||
Floating point operations in `no_std` use [`libm`](https://crates.io/crates/libm), which sometimes panics in debug mode (https://github.com/japaric/libm/issues/4).
|
Floating point operations in `no_std` use [`libm`](https://crates.io/crates/libm), which sometimes panics in debug mode (https://github.com/japaric/libm/issues/4).
|
||||||
So make sure to either use release builds or avoid WASM with floating point operations, for example by using [`deny_floating_point`](https://docs.rs/wasmi/0.4.0/wasmi/struct.Module.html#method.deny_floating_point).
|
So make sure to either use release builds or avoid WASM with floating point operations, for example by using [`deny_floating_point`](https://docs.rs/wasmi/0.4.0/wasmi/struct.Module.html#method.deny_floating_point).
|
||||||
|
|
||||||
|
# Thread-safe support
|
||||||
|
|
||||||
|
This crate supports thread-safe environments.
|
||||||
|
Enable the `threadsafe` feature and Rust's thread-safe data structures will be used.
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
parity-wasm = {
|
||||||
|
version = "0.31",
|
||||||
|
default-features = true,
|
||||||
|
features = "threadsafe"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
`wasmi` is primarily distributed under the terms of both the MIT
|
`wasmi` is primarily distributed under the terms of both the MIT
|
||||||
|
|
23
src/func.rs
23
src/func.rs
|
@ -1,6 +1,5 @@
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use alloc::prelude::v1::*;
|
use alloc::prelude::v1::*;
|
||||||
use alloc::rc::{Rc, Weak};
|
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use host::Externals;
|
use host::Externals;
|
||||||
use isa;
|
use isa;
|
||||||
|
@ -17,7 +16,7 @@ use {Signature, Trap};
|
||||||
///
|
///
|
||||||
/// [`FuncInstance`]: struct.FuncInstance.html
|
/// [`FuncInstance`]: struct.FuncInstance.html
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct FuncRef(Rc<FuncInstance>);
|
pub struct FuncRef(::MyRc<FuncInstance>);
|
||||||
|
|
||||||
impl ::core::ops::Deref for FuncRef {
|
impl ::core::ops::Deref for FuncRef {
|
||||||
type Target = FuncInstance;
|
type Target = FuncInstance;
|
||||||
|
@ -45,9 +44,9 @@ pub struct FuncInstance(FuncInstanceInternal);
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) enum FuncInstanceInternal {
|
pub(crate) enum FuncInstanceInternal {
|
||||||
Internal {
|
Internal {
|
||||||
signature: Rc<Signature>,
|
signature: ::MyRc<Signature>,
|
||||||
module: Weak<ModuleInstance>,
|
module: ::MyWeak<ModuleInstance>,
|
||||||
body: Rc<FuncBody>,
|
body: ::MyRc<FuncBody>,
|
||||||
},
|
},
|
||||||
Host {
|
Host {
|
||||||
signature: Signature,
|
signature: Signature,
|
||||||
|
@ -84,7 +83,7 @@ impl FuncInstance {
|
||||||
signature,
|
signature,
|
||||||
host_func_index,
|
host_func_index,
|
||||||
};
|
};
|
||||||
FuncRef(Rc::new(FuncInstance(func)))
|
FuncRef(::MyRc::new(FuncInstance(func)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns [signature] of this function instance.
|
/// Returns [signature] of this function instance.
|
||||||
|
@ -104,21 +103,21 @@ impl FuncInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn alloc_internal(
|
pub(crate) fn alloc_internal(
|
||||||
module: Weak<ModuleInstance>,
|
module: ::MyWeak<ModuleInstance>,
|
||||||
signature: Rc<Signature>,
|
signature: ::MyRc<Signature>,
|
||||||
body: FuncBody,
|
body: FuncBody,
|
||||||
) -> FuncRef {
|
) -> FuncRef {
|
||||||
let func = FuncInstanceInternal::Internal {
|
let func = FuncInstanceInternal::Internal {
|
||||||
signature,
|
signature,
|
||||||
module: module,
|
module: module,
|
||||||
body: Rc::new(body),
|
body: ::MyRc::new(body),
|
||||||
};
|
};
|
||||||
FuncRef(Rc::new(FuncInstance(func)))
|
FuncRef(::MyRc::new(FuncInstance(func)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn body(&self) -> Option<Rc<FuncBody>> {
|
pub(crate) fn body(&self) -> Option<::MyRc<FuncBody>> {
|
||||||
match *self.as_internal() {
|
match *self.as_internal() {
|
||||||
FuncInstanceInternal::Internal { ref body, .. } => Some(Rc::clone(body)),
|
FuncInstanceInternal::Internal { ref body, .. } => Some(::MyRc::clone(body)),
|
||||||
FuncInstanceInternal::Host { .. } => None,
|
FuncInstanceInternal::Host { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use alloc::rc::Rc;
|
|
||||||
use core::cell::Cell;
|
|
||||||
use parity_wasm::elements::ValueType as EValueType;
|
use parity_wasm::elements::ValueType as EValueType;
|
||||||
use types::ValueType;
|
use types::ValueType;
|
||||||
use value::RuntimeValue;
|
use value::RuntimeValue;
|
||||||
|
@ -11,7 +9,7 @@ use Error;
|
||||||
///
|
///
|
||||||
/// [`GlobalInstance`]: struct.GlobalInstance.html
|
/// [`GlobalInstance`]: struct.GlobalInstance.html
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct GlobalRef(Rc<GlobalInstance>);
|
pub struct GlobalRef(::MyRc<GlobalInstance>);
|
||||||
|
|
||||||
impl ::core::ops::Deref for GlobalRef {
|
impl ::core::ops::Deref for GlobalRef {
|
||||||
type Target = GlobalInstance;
|
type Target = GlobalInstance;
|
||||||
|
@ -33,7 +31,7 @@ impl ::core::ops::Deref for GlobalRef {
|
||||||
/// [`I64`]: enum.RuntimeValue.html#variant.I64
|
/// [`I64`]: enum.RuntimeValue.html#variant.I64
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct GlobalInstance {
|
pub struct GlobalInstance {
|
||||||
val: Cell<RuntimeValue>,
|
val: ::MyCell<RuntimeValue>,
|
||||||
mutable: bool,
|
mutable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,8 +41,8 @@ impl GlobalInstance {
|
||||||
/// Since it is possible to export only immutable globals,
|
/// Since it is possible to export only immutable globals,
|
||||||
/// users likely want to set `mutable` to `false`.
|
/// users likely want to set `mutable` to `false`.
|
||||||
pub fn alloc(val: RuntimeValue, mutable: bool) -> GlobalRef {
|
pub fn alloc(val: RuntimeValue, mutable: bool) -> GlobalRef {
|
||||||
GlobalRef(Rc::new(GlobalInstance {
|
GlobalRef(::MyRc::new(GlobalInstance {
|
||||||
val: Cell::new(val),
|
val: ::MyCell::new(val),
|
||||||
mutable,
|
mutable,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
12
src/lib.rs
12
src/lib.rs
|
@ -396,6 +396,12 @@ mod table;
|
||||||
mod types;
|
mod types;
|
||||||
mod value;
|
mod value;
|
||||||
|
|
||||||
|
#[cfg(feature = "threadsafe")]
|
||||||
|
mod threadsafe;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "threadsafe"))]
|
||||||
|
mod not_threadsafe;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
@ -410,6 +416,12 @@ pub use self::table::{TableInstance, TableRef};
|
||||||
pub use self::types::{GlobalDescriptor, MemoryDescriptor, Signature, TableDescriptor, ValueType};
|
pub use self::types::{GlobalDescriptor, MemoryDescriptor, Signature, TableDescriptor, ValueType};
|
||||||
pub use self::value::{Error as ValueError, FromRuntimeValue, LittleEndianConvert, RuntimeValue};
|
pub use self::value::{Error as ValueError, FromRuntimeValue, LittleEndianConvert, RuntimeValue};
|
||||||
|
|
||||||
|
#[cfg(feature = "threadsafe")]
|
||||||
|
pub use self::threadsafe::*;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "threadsafe"))]
|
||||||
|
pub use self::not_threadsafe::*;
|
||||||
|
|
||||||
/// WebAssembly-specific sizes and units.
|
/// WebAssembly-specific sizes and units.
|
||||||
pub mod memory_units {
|
pub mod memory_units {
|
||||||
pub use memory_units_crate::wasm32::*;
|
pub use memory_units_crate::wasm32::*;
|
||||||
|
|
115
src/memory.rs
115
src/memory.rs
|
@ -1,12 +1,6 @@
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use alloc::prelude::v1::*;
|
use alloc::prelude::v1::*;
|
||||||
use alloc::rc::Rc;
|
use core::{cmp, fmt, ops::Range, u32};
|
||||||
use core::{
|
|
||||||
cell::{Cell, RefCell},
|
|
||||||
cmp, fmt,
|
|
||||||
ops::Range,
|
|
||||||
u32,
|
|
||||||
};
|
|
||||||
use memory_units::{Bytes, Pages, RoundUpTo};
|
use memory_units::{Bytes, Pages, RoundUpTo};
|
||||||
use parity_wasm::elements::ResizableLimits;
|
use parity_wasm::elements::ResizableLimits;
|
||||||
use value::LittleEndianConvert;
|
use value::LittleEndianConvert;
|
||||||
|
@ -26,7 +20,7 @@ pub const LINEAR_MEMORY_PAGE_SIZE: Bytes = Bytes(65536);
|
||||||
/// [`MemoryInstance`]: struct.MemoryInstance.html
|
/// [`MemoryInstance`]: struct.MemoryInstance.html
|
||||||
///
|
///
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct MemoryRef(Rc<MemoryInstance>);
|
pub struct MemoryRef(::MyRc<MemoryInstance>);
|
||||||
|
|
||||||
impl ::core::ops::Deref for MemoryRef {
|
impl ::core::ops::Deref for MemoryRef {
|
||||||
type Target = MemoryInstance;
|
type Target = MemoryInstance;
|
||||||
|
@ -52,11 +46,11 @@ pub struct MemoryInstance {
|
||||||
/// Memory limits.
|
/// Memory limits.
|
||||||
limits: ResizableLimits,
|
limits: ResizableLimits,
|
||||||
/// Linear memory buffer with lazy allocation.
|
/// Linear memory buffer with lazy allocation.
|
||||||
buffer: RefCell<Vec<u8>>,
|
buffer: ::MyRefCell<Vec<u8>>,
|
||||||
initial: Pages,
|
initial: Pages,
|
||||||
current_size: Cell<usize>,
|
current_size: ::MyCell<usize>,
|
||||||
maximum: Option<Pages>,
|
maximum: Option<Pages>,
|
||||||
lowest_used: Cell<u32>,
|
lowest_used: ::MyCell<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for MemoryInstance {
|
impl fmt::Debug for MemoryInstance {
|
||||||
|
@ -127,7 +121,7 @@ impl MemoryInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
let memory = MemoryInstance::new(initial, maximum);
|
let memory = MemoryInstance::new(initial, maximum);
|
||||||
Ok(MemoryRef(Rc::new(memory)))
|
Ok(MemoryRef(::MyRc::new(memory)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create new linear memory instance.
|
/// Create new linear memory instance.
|
||||||
|
@ -137,11 +131,11 @@ impl MemoryInstance {
|
||||||
let initial_size: Bytes = initial.into();
|
let initial_size: Bytes = initial.into();
|
||||||
MemoryInstance {
|
MemoryInstance {
|
||||||
limits: limits,
|
limits: limits,
|
||||||
buffer: RefCell::new(Vec::with_capacity(4096)),
|
buffer: ::MyRefCell::new(Vec::with_capacity(4096)),
|
||||||
initial: initial,
|
initial: initial,
|
||||||
current_size: Cell::new(initial_size.0),
|
current_size: ::MyCell::new(initial_size.0),
|
||||||
maximum: maximum,
|
maximum: maximum,
|
||||||
lowest_used: Cell::new(u32::max_value()),
|
lowest_used: ::MyCell::new(u32::max_value()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,9 +198,9 @@ impl MemoryInstance {
|
||||||
|
|
||||||
/// Get value from memory at given offset.
|
/// Get value from memory at given offset.
|
||||||
pub fn get_value<T: LittleEndianConvert>(&self, offset: u32) -> Result<T, Error> {
|
pub fn get_value<T: LittleEndianConvert>(&self, offset: u32) -> Result<T, Error> {
|
||||||
let mut buffer = self.buffer.borrow_mut();
|
let region = self.checked_region(offset as usize, ::core::mem::size_of::<T>())?;
|
||||||
let region =
|
|
||||||
self.checked_region(&mut buffer, offset as usize, ::core::mem::size_of::<T>())?;
|
let buffer = self.buffer.borrow();
|
||||||
Ok(T::from_little_endian(&buffer[region.range()]).expect("Slice size is checked"))
|
Ok(T::from_little_endian(&buffer[region.range()]).expect("Slice size is checked"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,9 +211,9 @@ impl MemoryInstance {
|
||||||
///
|
///
|
||||||
/// [`get_into`]: #method.get_into
|
/// [`get_into`]: #method.get_into
|
||||||
pub fn get(&self, offset: u32, size: usize) -> Result<Vec<u8>, Error> {
|
pub fn get(&self, offset: u32, size: usize) -> Result<Vec<u8>, Error> {
|
||||||
let mut buffer = self.buffer.borrow_mut();
|
let region = self.checked_region(offset as usize, size)?;
|
||||||
let region = self.checked_region(&mut buffer, offset as usize, size)?;
|
|
||||||
|
|
||||||
|
let buffer = self.buffer.borrow();
|
||||||
Ok(buffer[region.range()].to_vec())
|
Ok(buffer[region.range()].to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,9 +223,9 @@ impl MemoryInstance {
|
||||||
///
|
///
|
||||||
/// Returns `Err` if the specified region is out of bounds.
|
/// Returns `Err` if the specified region is out of bounds.
|
||||||
pub fn get_into(&self, offset: u32, target: &mut [u8]) -> Result<(), Error> {
|
pub fn get_into(&self, offset: u32, target: &mut [u8]) -> Result<(), Error> {
|
||||||
let mut buffer = self.buffer.borrow_mut();
|
let region = self.checked_region(offset as usize, target.len())?;
|
||||||
let region = self.checked_region(&mut buffer, offset as usize, target.len())?;
|
|
||||||
|
|
||||||
|
let buffer = self.buffer.borrow();
|
||||||
target.copy_from_slice(&buffer[region.range()]);
|
target.copy_from_slice(&buffer[region.range()]);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -239,14 +233,12 @@ impl MemoryInstance {
|
||||||
|
|
||||||
/// Copy data in the memory at given offset.
|
/// Copy data in the memory at given offset.
|
||||||
pub fn set(&self, offset: u32, value: &[u8]) -> Result<(), Error> {
|
pub fn set(&self, offset: u32, value: &[u8]) -> Result<(), Error> {
|
||||||
let mut buffer = self.buffer.borrow_mut();
|
let range = self.checked_region(offset as usize, value.len())?.range();
|
||||||
let range = self
|
|
||||||
.checked_region(&mut buffer, offset as usize, value.len())?
|
|
||||||
.range();
|
|
||||||
|
|
||||||
if offset < self.lowest_used.get() {
|
if offset < self.lowest_used.get() {
|
||||||
self.lowest_used.set(offset);
|
self.lowest_used.set(offset);
|
||||||
}
|
}
|
||||||
|
let mut buffer = self.buffer.borrow_mut();
|
||||||
buffer[range].copy_from_slice(value);
|
buffer[range].copy_from_slice(value);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -254,13 +246,13 @@ impl MemoryInstance {
|
||||||
|
|
||||||
/// Copy value in the memory at given offset.
|
/// Copy value in the memory at given offset.
|
||||||
pub fn set_value<T: LittleEndianConvert>(&self, offset: u32, value: T) -> Result<(), Error> {
|
pub fn set_value<T: LittleEndianConvert>(&self, offset: u32, value: T) -> Result<(), Error> {
|
||||||
let mut buffer = self.buffer.borrow_mut();
|
|
||||||
let range = self
|
let range = self
|
||||||
.checked_region(&mut buffer, offset as usize, ::core::mem::size_of::<T>())?
|
.checked_region(offset as usize, ::core::mem::size_of::<T>())?
|
||||||
.range();
|
.range();
|
||||||
if offset < self.lowest_used.get() {
|
if offset < self.lowest_used.get() {
|
||||||
self.lowest_used.set(offset);
|
self.lowest_used.set(offset);
|
||||||
}
|
}
|
||||||
|
let mut buffer = self.buffer.borrow_mut();
|
||||||
value.into_little_endian(&mut buffer[range]);
|
value.into_little_endian(&mut buffer[range]);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -299,15 +291,8 @@ impl MemoryInstance {
|
||||||
Ok(size_before_grow)
|
Ok(size_before_grow)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn checked_region<B>(
|
fn checked_region(&self, offset: usize, size: usize) -> Result<CheckedRegion, Error> {
|
||||||
&self,
|
let mut buffer = self.buffer.borrow_mut();
|
||||||
buffer: &mut B,
|
|
||||||
offset: usize,
|
|
||||||
size: usize,
|
|
||||||
) -> Result<CheckedRegion, Error>
|
|
||||||
where
|
|
||||||
B: ::core::ops::DerefMut<Target = Vec<u8>>,
|
|
||||||
{
|
|
||||||
let end = offset.checked_add(size).ok_or_else(|| {
|
let end = offset.checked_add(size).ok_or_else(|| {
|
||||||
Error::Memory(format!(
|
Error::Memory(format!(
|
||||||
"trying to access memory block of size {} from offset {}",
|
"trying to access memory block of size {} from offset {}",
|
||||||
|
@ -334,17 +319,13 @@ impl MemoryInstance {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn checked_region_pair<B>(
|
fn checked_region_pair(
|
||||||
&self,
|
&self,
|
||||||
buffer: &mut B,
|
|
||||||
offset1: usize,
|
offset1: usize,
|
||||||
size1: usize,
|
size1: usize,
|
||||||
offset2: usize,
|
offset2: usize,
|
||||||
size2: usize,
|
size2: usize,
|
||||||
) -> Result<(CheckedRegion, CheckedRegion), Error>
|
) -> Result<(CheckedRegion, CheckedRegion), Error> {
|
||||||
where
|
|
||||||
B: ::core::ops::DerefMut<Target = Vec<u8>>,
|
|
||||||
{
|
|
||||||
let end1 = offset1.checked_add(size1).ok_or_else(|| {
|
let end1 = offset1.checked_add(size1).ok_or_else(|| {
|
||||||
Error::Memory(format!(
|
Error::Memory(format!(
|
||||||
"trying to access memory block of size {} from offset {}",
|
"trying to access memory block of size {} from offset {}",
|
||||||
|
@ -359,6 +340,7 @@ impl MemoryInstance {
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let mut buffer = self.buffer.borrow_mut();
|
||||||
let max = cmp::max(end1, end2);
|
let max = cmp::max(end1, end2);
|
||||||
if max <= self.current_size.get() && buffer.len() < max {
|
if max <= self.current_size.get() && buffer.len() < max {
|
||||||
buffer.resize(max, 0);
|
buffer.resize(max, 0);
|
||||||
|
@ -402,15 +384,14 @@ impl MemoryInstance {
|
||||||
///
|
///
|
||||||
/// Returns `Err` if either of specified regions is out of bounds.
|
/// Returns `Err` if either of specified regions is out of bounds.
|
||||||
pub fn copy(&self, src_offset: usize, dst_offset: usize, len: usize) -> Result<(), Error> {
|
pub fn copy(&self, src_offset: usize, dst_offset: usize, len: usize) -> Result<(), Error> {
|
||||||
let mut buffer = self.buffer.borrow_mut();
|
|
||||||
|
|
||||||
let (read_region, write_region) =
|
let (read_region, write_region) =
|
||||||
self.checked_region_pair(&mut buffer, src_offset, len, dst_offset, len)?;
|
self.checked_region_pair(src_offset, len, dst_offset, len)?;
|
||||||
|
|
||||||
if dst_offset < self.lowest_used.get() as usize {
|
if dst_offset < self.lowest_used.get() as usize {
|
||||||
self.lowest_used.set(dst_offset as u32);
|
self.lowest_used.set(dst_offset as u32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut buffer = self.buffer.borrow_mut();
|
||||||
unsafe {
|
unsafe {
|
||||||
::core::ptr::copy(
|
::core::ptr::copy(
|
||||||
buffer[read_region.range()].as_ptr(),
|
buffer[read_region.range()].as_ptr(),
|
||||||
|
@ -439,10 +420,8 @@ impl MemoryInstance {
|
||||||
dst_offset: usize,
|
dst_offset: usize,
|
||||||
len: usize,
|
len: usize,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut buffer = self.buffer.borrow_mut();
|
|
||||||
|
|
||||||
let (read_region, write_region) =
|
let (read_region, write_region) =
|
||||||
self.checked_region_pair(&mut buffer, src_offset, len, dst_offset, len)?;
|
self.checked_region_pair(src_offset, len, dst_offset, len)?;
|
||||||
|
|
||||||
if read_region.intersects(&write_region) {
|
if read_region.intersects(&write_region) {
|
||||||
return Err(Error::Memory(format!(
|
return Err(Error::Memory(format!(
|
||||||
|
@ -454,6 +433,7 @@ impl MemoryInstance {
|
||||||
self.lowest_used.set(dst_offset as u32);
|
self.lowest_used.set(dst_offset as u32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut buffer = self.buffer.borrow_mut();
|
||||||
unsafe {
|
unsafe {
|
||||||
::core::ptr::copy_nonoverlapping(
|
::core::ptr::copy_nonoverlapping(
|
||||||
buffer[read_region.range()].as_ptr(),
|
buffer[read_region.range()].as_ptr(),
|
||||||
|
@ -475,28 +455,21 @@ impl MemoryInstance {
|
||||||
dst_offset: usize,
|
dst_offset: usize,
|
||||||
len: usize,
|
len: usize,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
if Rc::ptr_eq(&src.0, &dst.0) {
|
if ::MyRc::ptr_eq(&src.0, &dst.0) {
|
||||||
// `transfer` is invoked with with same source and destination. Let's assume that regions may
|
// `transfer` is invoked with with same source and destination. Let's assume that regions may
|
||||||
// overlap and use `copy`.
|
// overlap and use `copy`.
|
||||||
return src.copy(src_offset, dst_offset, len);
|
return src.copy(src_offset, dst_offset, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Because memory references point to different memory instances, it is safe to `borrow_mut`
|
let src_range = src.checked_region(src_offset, len)?.range();
|
||||||
// both buffers at once (modulo `with_direct_access_mut`).
|
let dst_range = dst.checked_region(dst_offset, len)?.range();
|
||||||
let mut src_buffer = src.buffer.borrow_mut();
|
|
||||||
let mut dst_buffer = dst.buffer.borrow_mut();
|
|
||||||
|
|
||||||
let src_range = src
|
|
||||||
.checked_region(&mut src_buffer, src_offset, len)?
|
|
||||||
.range();
|
|
||||||
let dst_range = dst
|
|
||||||
.checked_region(&mut dst_buffer, dst_offset, len)?
|
|
||||||
.range();
|
|
||||||
|
|
||||||
if dst_offset < dst.lowest_used.get() as usize {
|
if dst_offset < dst.lowest_used.get() as usize {
|
||||||
dst.lowest_used.set(dst_offset as u32);
|
dst.lowest_used.set(dst_offset as u32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut dst_buffer = dst.buffer.borrow_mut();
|
||||||
|
let src_buffer = src.buffer.borrow();
|
||||||
dst_buffer[dst_range].copy_from_slice(&src_buffer[src_range]);
|
dst_buffer[dst_range].copy_from_slice(&src_buffer[src_range]);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -510,14 +483,13 @@ impl MemoryInstance {
|
||||||
///
|
///
|
||||||
/// Returns `Err` if the specified region is out of bounds.
|
/// Returns `Err` if the specified region is out of bounds.
|
||||||
pub fn clear(&self, offset: usize, new_val: u8, len: usize) -> Result<(), Error> {
|
pub fn clear(&self, offset: usize, new_val: u8, len: usize) -> Result<(), Error> {
|
||||||
let mut buffer = self.buffer.borrow_mut();
|
let range = self.checked_region(offset, len)?.range();
|
||||||
|
|
||||||
let range = self.checked_region(&mut buffer, offset, len)?.range();
|
|
||||||
|
|
||||||
if offset < self.lowest_used.get() as usize {
|
if offset < self.lowest_used.get() as usize {
|
||||||
self.lowest_used.set(offset as u32);
|
self.lowest_used.set(offset as u32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut buffer = self.buffer.borrow_mut();
|
||||||
for val in &mut buffer[range] {
|
for val in &mut buffer[range] {
|
||||||
*val = new_val
|
*val = new_val
|
||||||
}
|
}
|
||||||
|
@ -569,7 +541,6 @@ mod tests {
|
||||||
|
|
||||||
use super::{MemoryInstance, MemoryRef, LINEAR_MEMORY_PAGE_SIZE};
|
use super::{MemoryInstance, MemoryRef, LINEAR_MEMORY_PAGE_SIZE};
|
||||||
use memory_units::Pages;
|
use memory_units::Pages;
|
||||||
use std::rc::Rc;
|
|
||||||
use Error;
|
use Error;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -671,8 +642,8 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transfer_works() {
|
fn transfer_works() {
|
||||||
let src = MemoryRef(Rc::new(create_memory(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])));
|
let src = MemoryRef(::MyRc::new(create_memory(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])));
|
||||||
let dst = MemoryRef(Rc::new(create_memory(&[
|
let dst = MemoryRef(::MyRc::new(create_memory(&[
|
||||||
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
||||||
])));
|
])));
|
||||||
|
|
||||||
|
@ -687,7 +658,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transfer_still_works_with_same_memory() {
|
fn transfer_still_works_with_same_memory() {
|
||||||
let src = MemoryRef(Rc::new(create_memory(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])));
|
let src = MemoryRef(::MyRc::new(create_memory(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])));
|
||||||
|
|
||||||
MemoryInstance::transfer(&src, 4, &src, 0, 3).unwrap();
|
MemoryInstance::transfer(&src, 4, &src, 0, 3).unwrap();
|
||||||
|
|
||||||
|
@ -696,7 +667,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transfer_oob_with_same_memory_errors() {
|
fn transfer_oob_with_same_memory_errors() {
|
||||||
let src = MemoryRef(Rc::new(create_memory(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])));
|
let src = MemoryRef(::MyRc::new(create_memory(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])));
|
||||||
assert!(MemoryInstance::transfer(&src, 65535, &src, 0, 3).is_err());
|
assert!(MemoryInstance::transfer(&src, 65535, &src, 0, 3).is_err());
|
||||||
|
|
||||||
// Check that memories content left untouched
|
// Check that memories content left untouched
|
||||||
|
@ -705,8 +676,8 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transfer_oob_errors() {
|
fn transfer_oob_errors() {
|
||||||
let src = MemoryRef(Rc::new(create_memory(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])));
|
let src = MemoryRef(::MyRc::new(create_memory(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])));
|
||||||
let dst = MemoryRef(Rc::new(create_memory(&[
|
let dst = MemoryRef(::MyRc::new(create_memory(&[
|
||||||
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
||||||
])));
|
])));
|
||||||
|
|
||||||
|
@ -756,6 +727,8 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this test works only in the non-thread-safe variant, it deadlocks otherwise.
|
||||||
|
#[cfg(not(feature = "threadsafe"))]
|
||||||
#[should_panic]
|
#[should_panic]
|
||||||
#[test]
|
#[test]
|
||||||
fn zero_copy_panics_on_nested_access() {
|
fn zero_copy_panics_on_nested_access() {
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use alloc::prelude::v1::*;
|
use alloc::prelude::v1::*;
|
||||||
use alloc::rc::Rc;
|
|
||||||
use core::cell::RefCell;
|
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use Trap;
|
use Trap;
|
||||||
|
|
||||||
use alloc::collections::BTreeMap;
|
use alloc::collections::BTreeMap;
|
||||||
|
|
||||||
use core::cell::Ref;
|
|
||||||
use func::{FuncBody, FuncInstance, FuncRef};
|
use func::{FuncBody, FuncInstance, FuncRef};
|
||||||
use global::{GlobalInstance, GlobalRef};
|
use global::{GlobalInstance, GlobalRef};
|
||||||
use host::Externals;
|
use host::Externals;
|
||||||
|
@ -35,7 +32,7 @@ use {Error, MemoryInstance, Module, RuntimeValue, Signature, TableInstance};
|
||||||
///
|
///
|
||||||
/// [`ModuleInstance`]: struct.ModuleInstance.html
|
/// [`ModuleInstance`]: struct.ModuleInstance.html
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ModuleRef(pub(crate) Rc<ModuleInstance>);
|
pub struct ModuleRef(pub(crate) ::MyRc<ModuleInstance>);
|
||||||
|
|
||||||
impl ::core::ops::Deref for ModuleRef {
|
impl ::core::ops::Deref for ModuleRef {
|
||||||
type Target = ModuleInstance;
|
type Target = ModuleInstance;
|
||||||
|
@ -154,23 +151,23 @@ impl ExternVal {
|
||||||
/// [`invoke_export`]: #method.invoke_export
|
/// [`invoke_export`]: #method.invoke_export
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ModuleInstance {
|
pub struct ModuleInstance {
|
||||||
signatures: RefCell<Vec<Rc<Signature>>>,
|
signatures: ::MyRefCell<Vec<::MyRc<Signature>>>,
|
||||||
tables: RefCell<Vec<TableRef>>,
|
tables: ::MyRefCell<Vec<TableRef>>,
|
||||||
funcs: RefCell<Vec<FuncRef>>,
|
funcs: ::MyRefCell<Vec<FuncRef>>,
|
||||||
memories: RefCell<Vec<MemoryRef>>,
|
memories: ::MyRefCell<Vec<MemoryRef>>,
|
||||||
globals: RefCell<Vec<GlobalRef>>,
|
globals: ::MyRefCell<Vec<GlobalRef>>,
|
||||||
exports: RefCell<BTreeMap<String, ExternVal>>,
|
exports: ::MyRefCell<BTreeMap<String, ExternVal>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModuleInstance {
|
impl ModuleInstance {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
ModuleInstance {
|
ModuleInstance {
|
||||||
funcs: RefCell::new(Vec::new()),
|
funcs: ::MyRefCell::new(Vec::new()),
|
||||||
signatures: RefCell::new(Vec::new()),
|
signatures: ::MyRefCell::new(Vec::new()),
|
||||||
tables: RefCell::new(Vec::new()),
|
tables: ::MyRefCell::new(Vec::new()),
|
||||||
memories: RefCell::new(Vec::new()),
|
memories: ::MyRefCell::new(Vec::new()),
|
||||||
globals: RefCell::new(Vec::new()),
|
globals: ::MyRefCell::new(Vec::new()),
|
||||||
exports: RefCell::new(BTreeMap::new()),
|
exports: ::MyRefCell::new(BTreeMap::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,7 +187,7 @@ impl ModuleInstance {
|
||||||
self.funcs.borrow().get(idx as usize).cloned()
|
self.funcs.borrow().get(idx as usize).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn signature_by_index(&self, idx: u32) -> Option<Rc<Signature>> {
|
pub(crate) fn signature_by_index(&self, idx: u32) -> Option<::MyRc<Signature>> {
|
||||||
self.signatures.borrow().get(idx as usize).cloned()
|
self.signatures.borrow().get(idx as usize).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +195,7 @@ impl ModuleInstance {
|
||||||
self.funcs.borrow_mut().push(func);
|
self.funcs.borrow_mut().push(func);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_signature(&self, signature: Rc<Signature>) {
|
fn push_signature(&self, signature: ::MyRc<Signature>) {
|
||||||
self.signatures.borrow_mut().push(signature)
|
self.signatures.borrow_mut().push(signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,7 +213,7 @@ impl ModuleInstance {
|
||||||
|
|
||||||
/// Access all globals. This is a non-standard API so it's unlikely to be
|
/// Access all globals. This is a non-standard API so it's unlikely to be
|
||||||
/// portable to other engines.
|
/// portable to other engines.
|
||||||
pub fn globals<'a>(&self) -> Ref<Vec<GlobalRef>> {
|
pub fn globals<'a>(&self) -> ::MyRefRead<Vec<GlobalRef>> {
|
||||||
self.globals.borrow()
|
self.globals.borrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,10 +226,10 @@ impl ModuleInstance {
|
||||||
extern_vals: I,
|
extern_vals: I,
|
||||||
) -> Result<ModuleRef, Error> {
|
) -> Result<ModuleRef, Error> {
|
||||||
let module = loaded_module.module();
|
let module = loaded_module.module();
|
||||||
let instance = ModuleRef(Rc::new(ModuleInstance::default()));
|
let instance = ModuleRef(::MyRc::new(ModuleInstance::default()));
|
||||||
|
|
||||||
for &Type::Function(ref ty) in module.type_section().map(|ts| ts.types()).unwrap_or(&[]) {
|
for &Type::Function(ref ty) in module.type_section().map(|ts| ts.types()).unwrap_or(&[]) {
|
||||||
let signature = Rc::new(Signature::from_elements(ty));
|
let signature = ::MyRc::new(Signature::from_elements(ty));
|
||||||
instance.push_signature(signature);
|
instance.push_signature(signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,8 +323,11 @@ impl ModuleInstance {
|
||||||
locals: body.locals().to_vec(),
|
locals: body.locals().to_vec(),
|
||||||
code: code,
|
code: code,
|
||||||
};
|
};
|
||||||
let func_instance =
|
let func_instance = FuncInstance::alloc_internal(
|
||||||
FuncInstance::alloc_internal(Rc::downgrade(&instance.0), signature, func_body);
|
::MyRc::downgrade(&instance.0),
|
||||||
|
signature,
|
||||||
|
func_body,
|
||||||
|
);
|
||||||
instance.push_func(func_instance);
|
instance.push_func(func_instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
pub use alloc::rc::Rc as MyRc;
|
||||||
|
pub use alloc::rc::Weak as MyWeak;
|
||||||
|
pub use core::cell::Cell as MyCell;
|
||||||
|
pub use core::cell::Ref as MyRefRead;
|
||||||
|
pub use core::cell::Ref as MyRefWrite;
|
||||||
|
pub use core::cell::RefCell as MyRefCell;
|
10
src/table.rs
10
src/table.rs
|
@ -1,7 +1,5 @@
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use alloc::prelude::v1::*;
|
use alloc::prelude::v1::*;
|
||||||
use alloc::rc::Rc;
|
|
||||||
use core::cell::RefCell;
|
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::u32;
|
use core::u32;
|
||||||
use func::FuncRef;
|
use func::FuncRef;
|
||||||
|
@ -16,7 +14,7 @@ use Error;
|
||||||
/// [`TableInstance`]: struct.TableInstance.html
|
/// [`TableInstance`]: struct.TableInstance.html
|
||||||
///
|
///
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TableRef(Rc<TableInstance>);
|
pub struct TableRef(::MyRc<TableInstance>);
|
||||||
|
|
||||||
impl ::core::ops::Deref for TableRef {
|
impl ::core::ops::Deref for TableRef {
|
||||||
type Target = TableInstance;
|
type Target = TableInstance;
|
||||||
|
@ -42,7 +40,7 @@ pub struct TableInstance {
|
||||||
/// Table limits.
|
/// Table limits.
|
||||||
limits: ResizableLimits,
|
limits: ResizableLimits,
|
||||||
/// Table memory buffer.
|
/// Table memory buffer.
|
||||||
buffer: RefCell<Vec<Option<FuncRef>>>,
|
buffer: ::MyRefCell<Vec<Option<FuncRef>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for TableInstance {
|
impl fmt::Debug for TableInstance {
|
||||||
|
@ -67,13 +65,13 @@ impl TableInstance {
|
||||||
/// Returns `Err` if `initial_size` is greater than `maximum_size`.
|
/// Returns `Err` if `initial_size` is greater than `maximum_size`.
|
||||||
pub fn alloc(initial_size: u32, maximum_size: Option<u32>) -> Result<TableRef, Error> {
|
pub fn alloc(initial_size: u32, maximum_size: Option<u32>) -> Result<TableRef, Error> {
|
||||||
let table = TableInstance::new(ResizableLimits::new(initial_size, maximum_size))?;
|
let table = TableInstance::new(ResizableLimits::new(initial_size, maximum_size))?;
|
||||||
Ok(TableRef(Rc::new(table)))
|
Ok(TableRef(::MyRc::new(table)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(limits: ResizableLimits) -> Result<TableInstance, Error> {
|
fn new(limits: ResizableLimits) -> Result<TableInstance, Error> {
|
||||||
check_limits(&limits)?;
|
check_limits(&limits)?;
|
||||||
Ok(TableInstance {
|
Ok(TableInstance {
|
||||||
buffer: RefCell::new(vec![None; limits.initial() as usize]),
|
buffer: ::MyRefCell::new(vec![None; limits.initial() as usize]),
|
||||||
limits: limits,
|
limits: limits,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
extern crate atomic;
|
||||||
|
|
||||||
|
use alloc::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
pub use self::atomic::{Atomic, Ordering::Relaxed as Ordering};
|
||||||
|
pub use alloc::sync::{
|
||||||
|
Arc as MyRc, RwLockReadGuard as MyRefRead, RwLockWriteGuard as MyRefWrite, Weak as MyWeak,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Thread-safe wrapper which can be used in place of a `RefCell`.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MyRefCell<T>(Arc<RwLock<T>>);
|
||||||
|
|
||||||
|
impl<T> MyRefCell<T> {
|
||||||
|
/// Create new wrapper object.
|
||||||
|
pub fn new(obj: T) -> MyRefCell<T> {
|
||||||
|
MyRefCell(Arc::new(RwLock::new(obj)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrow a `MyRef` to the inner value.
|
||||||
|
pub fn borrow(&self) -> ::MyRefRead<T> {
|
||||||
|
self.0
|
||||||
|
.read()
|
||||||
|
.expect("failed to acquire lock while trying to borrow")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrow a mutable `MyRef` to the inner value.
|
||||||
|
pub fn borrow_mut(&self) -> ::MyRefWrite<T> {
|
||||||
|
self.0
|
||||||
|
.write()
|
||||||
|
.expect("failed to acquire lock while trying to borrow mutably")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Thread-safe wrapper which can be used in place of a `Cell`.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MyCell<T>(Atomic<T>)
|
||||||
|
where
|
||||||
|
T: Copy;
|
||||||
|
|
||||||
|
impl<T> MyCell<T>
|
||||||
|
where
|
||||||
|
T: Copy,
|
||||||
|
{
|
||||||
|
/// Create new wrapper object.
|
||||||
|
pub fn new(obj: T) -> MyCell<T> {
|
||||||
|
MyCell(Atomic::new(obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the inner value.
|
||||||
|
pub fn get(&self) -> T {
|
||||||
|
self.0.load(::Ordering)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the inner value.
|
||||||
|
pub fn set(&self, val: T) {
|
||||||
|
self.0.store(val, ::Ordering);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue