├── .gitignore ├── .travis.yml ├── COPYING ├── Cargo.toml ├── README.md ├── benches └── pipe.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /Cargo.lock 2 | /target/ 3 | /.gitattributes 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | sudo: false 7 | os: linux 8 | cache: 9 | directories: 10 | - $HOME/.cargo 11 | - target 12 | matrix: 13 | fast_finish: true 14 | allow_failures: 15 | - rust: nightly 16 | env: 17 | matrix: 18 | - CARGO_FEATURES= 19 | - CARGO_FEATURES=bidirectional 20 | global: 21 | - secure: "hrpQNYCQVQZu8Fem46jKa80UQsbj85BVO03MyBkM9+GdW+og4gSFKCt2S5m3Ac+7I5TXJSF4m6il1Z/eoydT/ISlVAw4MlKuouzChlhaA5dqkJJy1kGHRMgMJuc1HmKzVsLlvl8hZgtSGPwXjrVHZFYXa9/IFxCf0KD5r7Gv9Tmq8fNXI6pSamZnF3lN94JLjTKNCsUC/r0eccu1Nnfees/QqiCr4hRTyjKRiPyDnQqu5lYkZjaJqsMJQnagwqldk9VgixxQyYgYiPcMXdmA9DM5FJhtDUCHjyIwUxqf2FCtXSScwwAjjMMKTibmwZdvmZtLnVvwHtilGr7SbKeIGPqv/fsKms9AKUPmny3efu5Y5v3411tvSMJSUGd+PWpzqZVlaY0v4S7rAsf2BDlOpSHaXaujDvdAsO5bJHJeoQWpi9hItMZjM/TTCLfvttsln0LbDOuMvPYVrXMmABcoOcNPZKB1a/qqYAfR8cualX5kUPHSUCahAUPH3r9AzwT+BAovb2heLNwYMLa+Iv3T9vuceTkEijCAHTrYtChhNAUAlxA9YB/z/Qz7emfYq4Nbxv6dWzJuQIweM9p4KWefv9WMQXbkcIRrImcQetWailMIvtshvFwV6PC3JM3AXVhc/edEbPgv89Wd1osEqqLfV6O7lQvKXtzyHmI5wK8QcZQ=" 22 | before_script: 23 | - curl -L https://github.com/arcnmx/ci/archive/0.2.tar.gz | tar -xzC $HOME && . $HOME/ci-0.2/src 24 | script: 25 | - cargo build 26 | - cargo test 27 | - cargo bench -- --test 28 | - cargo doc 29 | - | 30 | [[ $TRAVIS_RUST_VERSION != nightly ]] || cargo doc --features unstable-doc-cfg 31 | deploy: 32 | provider: script 33 | script: "true" 34 | on: 35 | tags: true 36 | all_branches: true 37 | condition: "$TRAVIS_RUST_VERSION = stable && $CARGO_FEATURES == bidirectional" 38 | before_deploy: 39 | - cargo package 40 | after_deploy: 41 | - cargo publish 42 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 arcnmx 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pipe" 3 | version = "0.4.0" # keep in sync with html_root_url 4 | authors = ["arcnmx"] 5 | 6 | description = "Synchronous Read/Write memory pipe" 7 | documentation = "https://docs.rs/pipe/" 8 | repository = "https://github.com/arcnmx/pipe-rs" 9 | readme = "README.md" 10 | keywords = ["pipe", "synchronous", "read", "write"] 11 | license = "MIT" 12 | 13 | [lib] 14 | bench = false 15 | 16 | [[bench]] 17 | name = "pipe" 18 | harness = false 19 | 20 | [features] 21 | bidirectional = ["readwrite"] 22 | unstable-doc-cfg = [] 23 | 24 | [dependencies] 25 | crossbeam-channel = "^0.5.0" 26 | readwrite = { version = "^0.1.1", optional = true } 27 | 28 | [dev-dependencies] 29 | criterion = "^0.3.0" 30 | os_pipe = "^0.9.0" 31 | 32 | [package.metadata.docs.rs] 33 | features = ["bidirectional", "unstable-doc-cfg"] 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pipe 2 | 3 | [![travis-badge][]][travis] [![release-badge][]][cargo] [![docs-badge][]][docs] [![license-badge][]][license] 4 | 5 | A synchronous memory `Read`/`Write` pipe. 6 | 7 | [travis-badge]: https://img.shields.io/travis/arcnmx/pipe-rs/master.svg?style=flat-square 8 | [travis]: https://travis-ci.org/arcnmx/pipe-rs 9 | [release-badge]: https://img.shields.io/crates/v/pipe.svg?style=flat-square 10 | [cargo]: https://crates.io/crates/pipe 11 | [docs-badge]: https://img.shields.io/badge/API-docs-blue.svg?style=flat-square 12 | [docs]: https://docs.rs/pipe/ 13 | [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square 14 | [license]: https://github.com/arcnmx/pipe-rs/blob/master/COPYING 15 | -------------------------------------------------------------------------------- /benches/pipe.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | extern crate os_pipe; 4 | extern crate pipe; 5 | 6 | use criterion::{black_box, Bencher, Criterion, ParameterizedBenchmark, Throughput}; 7 | use std::convert::TryInto; 8 | use std::io::prelude::*; 9 | use std::io::BufWriter; 10 | use std::thread; 11 | 12 | const TOTAL_TO_SEND: usize = 1 * 1024 * 1024; 13 | 14 | fn send_recv_size(mut f: F) -> impl FnMut(&mut Bencher, &&(usize, usize)) 15 | where 16 | F: FnMut() -> (R, W), 17 | F: Send + 'static, 18 | R: Read + Send + 'static, 19 | W: Write + Send + 'static, 20 | { 21 | return move |b: &mut Bencher, &&(size, reads)| { 22 | let f = &mut f; 23 | let buf: Vec = (0..size).map(|i| i as u8).collect(); 24 | b.iter(move || { 25 | let (mut reader, mut writer) = f(); 26 | let t = thread::spawn(move || { 27 | let mut buf = vec![0; size / reads]; 28 | while let Ok(_) = reader.read_exact(&mut buf) {} 29 | }); 30 | 31 | for _ in 0..(TOTAL_TO_SEND / size) { 32 | writer.write_all(black_box(&buf[..])).unwrap(); 33 | } 34 | writer.flush().unwrap(); 35 | drop(writer); 36 | t.join().expect("writing failed"); 37 | }) 38 | }; 39 | } 40 | 41 | fn pipe_send(c: &mut Criterion) { 42 | const KB: usize = 1024; 43 | const SIZES: &[(usize, usize)] = &[ 44 | (4 * KB, 1), 45 | (4 * KB, 16), 46 | (8 * KB, 1), 47 | (16 * KB, 1), 48 | (32 * KB, 1), 49 | (64 * KB, 1), 50 | (64 * KB, 16), 51 | ]; 52 | let bench = ParameterizedBenchmark::new( 53 | "pipe-rs", 54 | send_recv_size(|| pipe::pipe()), 55 | SIZES, 56 | ); 57 | let bench = bench 58 | .with_function("pipe-rs-buffered", send_recv_size(|| pipe::pipe_buffered())) 59 | .with_function("pipe-rs-bufwrite", send_recv_size(|| { 60 | let (r, w) = pipe::pipe(); 61 | (r, BufWriter::new(w)) 62 | })) 63 | .with_function("os_pipe", send_recv_size(|| os_pipe::pipe().unwrap())) 64 | .throughput(|_| Throughput::Bytes(TOTAL_TO_SEND.try_into().unwrap())); 65 | c.bench("pipe_send", bench); 66 | } 67 | 68 | criterion_group!(benches, pipe_send); 69 | criterion_main!(benches); 70 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![doc(html_root_url = "https://docs.rs/pipe/0.4.0")] 3 | #![cfg_attr(feature = "unstable-doc-cfg", feature(doc_cfg))] 4 | 5 | //! Synchronous in-memory pipe 6 | //! 7 | //! ## Example 8 | //! 9 | //! ``` 10 | //! use std::thread::spawn; 11 | //! use std::io::{Read, Write}; 12 | //! 13 | //! let (mut read, mut write) = pipe::pipe(); 14 | //! 15 | //! let message = "Hello, world!"; 16 | //! spawn(move || write.write_all(message.as_bytes()).unwrap()); 17 | //! 18 | //! let mut s = String::new(); 19 | //! read.read_to_string(&mut s).unwrap(); 20 | //! 21 | //! assert_eq!(&s, message); 22 | //! ``` 23 | 24 | #[cfg(feature="readwrite")] 25 | extern crate readwrite; 26 | extern crate crossbeam_channel; 27 | 28 | use crossbeam_channel::{Sender, Receiver, SendError, TrySendError}; 29 | use std::io::{self, BufRead, Read, Write}; 30 | use std::cmp::min; 31 | use std::mem::replace; 32 | use std::hint::unreachable_unchecked; 33 | 34 | // value for libstd 35 | const DEFAULT_BUF_SIZE: usize = 8 * 1024; 36 | 37 | /// The `Read` end of a pipe (see `pipe()`) 38 | pub struct PipeReader { 39 | receiver: Receiver>, 40 | buffer: Vec, 41 | position: usize, 42 | } 43 | 44 | /// The `Write` end of a pipe (see `pipe()`) 45 | #[derive(Clone)] 46 | pub struct PipeWriter { 47 | sender: Sender> 48 | } 49 | 50 | /// The `Write` end of a pipe (see `pipe()`) that will buffer small writes before sending 51 | /// to the reader end. 52 | pub struct PipeBufWriter { 53 | sender: Option>>, 54 | buffer: Vec, 55 | size: usize, 56 | } 57 | 58 | /// Creates a synchronous memory pipe 59 | pub fn pipe() -> (PipeReader, PipeWriter) { 60 | let (sender, receiver) = crossbeam_channel::bounded(0); 61 | 62 | ( 63 | PipeReader { receiver, buffer: Vec::new(), position: 0 }, 64 | PipeWriter { sender }, 65 | ) 66 | } 67 | 68 | /// Creates a synchronous memory pipe with buffered writer 69 | pub fn pipe_buffered() -> (PipeReader, PipeBufWriter) { 70 | let (tx, rx) = crossbeam_channel::bounded(0); 71 | 72 | (PipeReader { receiver: rx, buffer: Vec::new(), position: 0 }, PipeBufWriter { sender: Some(tx), buffer: Vec::with_capacity(DEFAULT_BUF_SIZE), size: DEFAULT_BUF_SIZE } ) 73 | } 74 | 75 | /// Creates a pair of pipes for bidirectional communication, a bit like UNIX's `socketpair(2)`. 76 | #[cfg(feature = "bidirectional")] 77 | #[cfg_attr(feature = "unstable-doc-cfg", doc(cfg(feature = "bidirectional")))] 78 | pub fn bipipe() -> (readwrite::ReadWrite, readwrite::ReadWrite) { 79 | let (r1,w1) = pipe(); 80 | let (r2,w2) = pipe(); 81 | ((r1,w2).into(), (r2,w1).into()) 82 | } 83 | 84 | /// Creates a pair of pipes for bidirectional communication using buffered writer, a bit like UNIX's `socketpair(2)`. 85 | #[cfg(feature = "bidirectional")] 86 | #[cfg_attr(feature = "unstable-doc-cfg", doc(cfg(feature = "bidirectional")))] 87 | pub fn bipipe_buffered() -> (readwrite::ReadWrite, readwrite::ReadWrite) { 88 | let (r1,w1) = pipe_buffered(); 89 | let (r2,w2) = pipe_buffered(); 90 | ((r1,w2).into(), (r2,w1).into()) 91 | } 92 | 93 | fn epipe() -> io::Error { 94 | io::Error::new(io::ErrorKind::BrokenPipe, "pipe reader has been dropped") 95 | } 96 | 97 | impl PipeWriter { 98 | /// Extracts the inner `Sender` from the writer 99 | pub fn into_inner(self) -> Sender> { 100 | self.sender 101 | } 102 | 103 | /// Gets a reference to the underlying `Sender` 104 | pub fn sender(&self) -> &Sender> { 105 | &self.sender 106 | } 107 | 108 | /// Write data to the associated `PipeReader` 109 | pub fn send>>(&self, bytes: B) -> io::Result<()> { 110 | self.sender.send(bytes.into()) 111 | .map_err(|_| epipe()) 112 | .map(drop) 113 | } 114 | } 115 | 116 | impl PipeBufWriter { 117 | /// Extracts the inner `Sender` from the writer, and any pending buffered data 118 | pub fn into_inner(mut self) -> (Sender>, Vec) { 119 | let sender = match replace(&mut self.sender, None) { 120 | Some(sender) => sender, 121 | None => unsafe { 122 | // SAFETY: this is safe as long as `into_inner()` is the only method 123 | // that clears the sender 124 | unreachable_unchecked() 125 | }, 126 | }; 127 | (sender, replace(&mut self.buffer, Vec::new())) 128 | } 129 | 130 | #[inline] 131 | /// Gets a reference to the underlying `Sender` 132 | pub fn sender(&self) -> &Sender> { 133 | match &self.sender { 134 | Some(sender) => sender, 135 | None => unsafe { 136 | // SAFETY: this is safe as long as `into_inner()` is the only method 137 | // that clears the sender, and this fn is never called afterward 138 | unreachable_unchecked() 139 | }, 140 | } 141 | } 142 | 143 | /// Returns a reference to the internally buffered data. 144 | pub fn buffer(&self) -> &[u8] { 145 | &self.buffer 146 | } 147 | 148 | /// Returns the number of bytes the internal buffer can hold without flushing. 149 | pub fn capacity(&self) -> usize { 150 | self.size 151 | } 152 | } 153 | 154 | /// Creates a new handle to the `PipeBufWriter` with a fresh new buffer. Any pending data is still 155 | /// owned by the existing writer and should be flushed if necessary. 156 | impl Clone for PipeBufWriter { 157 | fn clone(&self) -> Self { 158 | Self { 159 | sender: self.sender.clone(), 160 | buffer: Vec::with_capacity(self.size), 161 | size: self.size, 162 | } 163 | } 164 | } 165 | 166 | impl PipeReader { 167 | /// Extracts the inner `Receiver` from the writer, and any pending buffered data 168 | pub fn into_inner(mut self) -> (Receiver>, Vec) { 169 | self.buffer.drain(..self.position); 170 | (self.receiver, self.buffer) 171 | } 172 | 173 | /// Returns a reference to the internally buffered data. 174 | pub fn buffer(&self) -> &[u8] { 175 | &self.buffer[self.position..] 176 | } 177 | } 178 | 179 | /// Creates a new handle to the `PipeReader` with a fresh new buffer. Any pending data is still 180 | /// owned by the existing reader and will not be accessible from the new handle. 181 | impl Clone for PipeReader { 182 | fn clone(&self) -> Self { 183 | Self { 184 | receiver: self.receiver.clone(), 185 | buffer: Vec::new(), 186 | position: 0, 187 | } 188 | } 189 | } 190 | 191 | impl BufRead for PipeReader { 192 | fn fill_buf(&mut self) -> io::Result<&[u8]> { 193 | while self.position >= self.buffer.len() { 194 | match self.receiver.recv() { 195 | // The only existing error is EOF 196 | Err(_) => break, 197 | Ok(data) => { 198 | self.buffer = data; 199 | self.position = 0; 200 | } 201 | } 202 | } 203 | 204 | Ok(&self.buffer[self.position..]) 205 | } 206 | 207 | fn consume(&mut self, amt: usize) { 208 | debug_assert!(self.buffer.len() - self.position >= amt); 209 | self.position += amt 210 | } 211 | } 212 | 213 | impl Read for PipeReader { 214 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 215 | if buf.is_empty() { 216 | return Ok(0); 217 | } 218 | 219 | let internal = self.fill_buf()?; 220 | 221 | let len = min(buf.len(), internal.len()); 222 | if len > 0 { 223 | buf[..len].copy_from_slice(&internal[..len]); 224 | self.consume(len); 225 | } 226 | Ok(len) 227 | } 228 | } 229 | 230 | impl Write for &'_ PipeWriter { 231 | fn write(&mut self, buf: &[u8]) -> io::Result { 232 | let data = buf.to_vec(); 233 | 234 | self.send(data) 235 | .map(|_| buf.len()) 236 | } 237 | 238 | fn flush(&mut self) -> io::Result<()> { 239 | Ok(()) 240 | } 241 | } 242 | 243 | impl Write for PipeWriter { 244 | #[inline] 245 | fn write(&mut self, buf: &[u8]) -> io::Result { 246 | Write::write(&mut &*self, buf) 247 | } 248 | 249 | #[inline] 250 | fn flush(&mut self) -> io::Result<()> { 251 | Write::flush(&mut &*self) 252 | } 253 | } 254 | 255 | impl Write for PipeBufWriter { 256 | fn write(&mut self, buf: &[u8]) -> io::Result { 257 | let buffer_len = self.buffer.len(); 258 | let bytes_written = if buf.len() > self.size { 259 | // bypass buffering for big writes 260 | buf.len() 261 | } else { 262 | // avoid resizing of the buffer 263 | min(buf.len(), self.size - buffer_len) 264 | }; 265 | self.buffer.extend_from_slice(&buf[..bytes_written]); 266 | 267 | if self.buffer.len() >= self.size { 268 | self.flush()?; 269 | } else { 270 | // reserve capacity later to avoid needless allocations 271 | let data = replace(&mut self.buffer, Vec::new()); 272 | 273 | // buffer still has space but try to send it in case the other side already awaits 274 | match self.sender().try_send(data) { 275 | Ok(_) => self.buffer.reserve(self.size), 276 | Err(TrySendError::Full(data)) => 277 | self.buffer = data, 278 | Err(TrySendError::Disconnected(data)) => { 279 | self.buffer = data; 280 | self.buffer.truncate(buffer_len); 281 | return Err(epipe()) 282 | }, 283 | } 284 | } 285 | 286 | Ok(bytes_written) 287 | } 288 | 289 | fn flush(&mut self) -> io::Result<()> { 290 | if self.buffer.is_empty() { 291 | Ok(()) 292 | } else { 293 | let data = replace(&mut self.buffer, Vec::new()); 294 | match self.sender().send(data) { 295 | Ok(_) => { 296 | self.buffer.reserve(self.size); 297 | Ok(()) 298 | }, 299 | Err(SendError(data)) => { 300 | self.buffer = data; 301 | Err(epipe()) 302 | }, 303 | } 304 | } 305 | } 306 | } 307 | 308 | /// Flushes the contents of the buffer before the writer is dropped. Errors are ignored, so it is 309 | /// recommended that `flush()` be used explicitly instead of relying on Drop. 310 | /// 311 | /// This final flush can be avoided by using `drop(writer.into_inner())`. 312 | impl Drop for PipeBufWriter { 313 | fn drop(&mut self) { 314 | if !self.buffer.is_empty() { 315 | let data = replace(&mut self.buffer, Vec::new()); 316 | let _ = self.sender().send(data); 317 | } 318 | } 319 | } 320 | 321 | #[cfg(test)] 322 | mod tests { 323 | use std::thread::spawn; 324 | use std::io::{Read, Write}; 325 | use super::*; 326 | 327 | #[test] 328 | fn pipe_reader() { 329 | let i = b"hello there"; 330 | let mut o = Vec::with_capacity(i.len()); 331 | let (mut r, mut w) = pipe(); 332 | let guard = spawn(move || { 333 | w.write_all(&i[..5]).unwrap(); 334 | w.write_all(&i[5..]).unwrap(); 335 | drop(w); 336 | }); 337 | 338 | r.read_to_end(&mut o).unwrap(); 339 | assert_eq!(i, &o[..]); 340 | 341 | guard.join().unwrap(); 342 | } 343 | 344 | #[test] 345 | fn pipe_writer_fail() { 346 | let i = b"hi"; 347 | let (r, mut w) = pipe(); 348 | let guard = spawn(move || { 349 | drop(r); 350 | }); 351 | 352 | assert!(w.write_all(i).is_err()); 353 | 354 | guard.join().unwrap(); 355 | } 356 | 357 | #[test] 358 | fn small_reads() { 359 | let block_cnt = 20; 360 | const BLOCK: usize = 20; 361 | let (mut r, mut w) = pipe(); 362 | let guard = spawn(move || { 363 | for _ in 0..block_cnt { 364 | let data = &[0; BLOCK]; 365 | w.write_all(data).unwrap(); 366 | } 367 | }); 368 | 369 | let mut buff = [0; BLOCK / 2]; 370 | let mut read = 0; 371 | while let Ok(size) = r.read(&mut buff) { 372 | // 0 means EOF 373 | if size == 0 { 374 | break; 375 | } 376 | read += size; 377 | } 378 | assert_eq!(block_cnt * BLOCK, read); 379 | 380 | guard.join().unwrap(); 381 | } 382 | 383 | #[test] 384 | fn pipe_reader_buffered() { 385 | let i = b"hello there"; 386 | let mut o = Vec::with_capacity(i.len()); 387 | let (mut r, mut w) = pipe_buffered(); 388 | let guard = spawn(move || { 389 | w.write_all(&i[..5]).unwrap(); 390 | w.write_all(&i[5..]).unwrap(); 391 | w.flush().unwrap(); 392 | drop(w); 393 | }); 394 | 395 | r.read_to_end(&mut o).unwrap(); 396 | assert_eq!(i, &o[..]); 397 | 398 | guard.join().unwrap(); 399 | } 400 | 401 | #[test] 402 | fn pipe_writer_fail_buffered() { 403 | let i = &[0; DEFAULT_BUF_SIZE * 2]; 404 | let (r, mut w) = pipe_buffered(); 405 | let guard = spawn(move || { 406 | drop(r); 407 | }); 408 | 409 | assert!(w.write_all(i).is_err()); 410 | 411 | guard.join().unwrap(); 412 | } 413 | 414 | #[test] 415 | fn small_reads_buffered() { 416 | let block_cnt = 20; 417 | const BLOCK: usize = 20; 418 | let (mut r, mut w) = pipe_buffered(); 419 | let guard = spawn(move || { 420 | for _ in 0..block_cnt { 421 | let data = &[0; BLOCK]; 422 | w.write_all(data).unwrap(); 423 | } 424 | w.flush().unwrap(); 425 | }); 426 | 427 | let mut buff = [0; BLOCK / 2]; 428 | let mut read = 0; 429 | while let Ok(size) = r.read(&mut buff) { 430 | // 0 means EOF 431 | if size == 0 { 432 | break; 433 | } 434 | read += size; 435 | } 436 | assert_eq!(block_cnt * BLOCK, read); 437 | 438 | guard.join().unwrap(); 439 | } 440 | } 441 | --------------------------------------------------------------------------------