├── .gitignore ├── .travis.yml ├── Cargo.toml ├── README.md ├── CHANGELOG.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | fast_finish: true 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "erlang_port" 3 | version = "0.2.0" 4 | authors = ["Graeme Coupar "] 5 | description = "Helper library for writing Elixir & Erlang ports in rust" 6 | homepage = "https://github.com/obmarg/erlang_port" 7 | repository = "https://github.com/obmarg/erlang_port" 8 | readme = "README.md" 9 | keywords = ["erlang", "elixir", "port", "interop"] 10 | license = "MIT" 11 | 12 | [badges] 13 | travis-ci = { "repository" = "obmarg/erlang_port" } 14 | maintenance = { status = "experimental" } 15 | 16 | [dependencies] 17 | byteorder = "1" 18 | serde = "1.0" 19 | serde_derive = "1.0" 20 | serde_eetf = "0" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # erlang_port 2 | 3 | A library to ease the process of writing Erlang/Elixir ports using Rust. 4 | 5 | ## Getting Started 6 | 7 | These instructions will get you a copy of the project up and running on your 8 | local machine for development and testing purposes. 9 | 10 | ### Prerequisites 11 | 12 | You will need a copy of Rust installed. Erlang, Elixir or another BEAM VM 13 | would also be useful, as this library is meant to interact with them. 14 | Installing these is outside the scope of this document though, so look 15 | elsewhere for instructions for that. 16 | 17 | ### Installing 18 | 19 | Add erlang_port to your `Cargo.toml` 20 | 21 | ``` 22 | erlang_port = "0" 23 | ``` 24 | 25 | ### Using 26 | 27 | Please see [the documentation](https://docs.rs/erlang_port/) for usage & examples. 28 | 29 | ## Running the tests 30 | 31 | ``` 32 | cargo test 33 | ``` 34 | 35 | ## Versioning 36 | 37 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see `CHANGELOG.md` 38 | 39 | ## License 40 | 41 | This project is licensed under the MIT License. 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is roughly based on [Keep a 6 | Changelog](http://keepachangelog.com/en/1.0.0/). 7 | 8 | This project intends to inhere to [Semantic 9 | Versioning](http://semver.org/spec/v2.0.0.html), but has not yet reached 1.0 so 10 | all APIs might be changed. 11 | 12 | ## Unreleased - yyyy-mm-dd 13 | 14 | ## Bug Fixes 15 | 16 | - Fixed a minor issue in the main module doccomment. 17 | 18 | ## v0.2.0 - 2018-10-14 19 | 20 | ### Breaking Changes 21 | 22 | - Completely refactored the interface to use structs and traits rather than top 23 | level functions. This should allow for users to implement their own protocols 24 | over traits in addition to the hard-coded 4 byte packet reading/writing that 25 | we had before. 26 | 27 | ### New Features 28 | 29 | - We now support writing ports that are opened in nouse_stdio mode. 30 | 31 | ## v0.1.0 - 2018-10-13 32 | 33 | The initial release. 34 | 35 | ### New Features 36 | 37 | - Initial implementation with ability to send and receieve commands. 38 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! erlang_port helps make writing Erlang/Elixir ports in Rust easier. 2 | //! 3 | //! Makes use of the `serde_eetf` crate to serialize/deserialize rust datatypes 4 | //! into erlang external term format, suitable for passing to/receiving from 5 | //! `binary_to_term`/`term_to_binary` 6 | //! 7 | //! Assuming you are starting your port in packet mode, it's recommended that 8 | //! you use the `stdio` or `nouse_stdio` functions inside the `main` fuction of 9 | //! your application to create an `IOPort`. You can then use the `sender` and 10 | //! `receiver` properties on that `IOPort` to communicate with Erlang/Elixir. 11 | //! 12 | //! For example, if you create the following rust program that reads strings 13 | //! from a port and returns them uppercased: 14 | //! 15 | //! ```rust,no_run 16 | //! fn upcase(mut s: String) -> Result { 17 | //! s.make_ascii_uppercase(); 18 | //! Ok(s) 19 | //! } 20 | //! 21 | //! fn main() { 22 | //! use erlang_port::{PortReceive, PortSend}; 23 | //! 24 | //! let mut port = unsafe { 25 | //! use erlang_port::PacketSize; 26 | //! erlang_port::nouse_stdio(PacketSize::Four) 27 | //! }; 28 | //! 29 | //! for string_in in port.receiver.iter() { 30 | //! let result = upcase(string_in); 31 | //! 32 | //! port.sender.reply(result); 33 | //! } 34 | //! } 35 | //! ``` 36 | //! 37 | //! Then you can call into this port from Elixir: 38 | //! 39 | //! ```elixir 40 | //! iex> port = 41 | //! ...> Port.open({:spawn_executable, port_path}, [ 42 | //! ...> {:packet, 4}, 43 | //! ...> :nouse_stdio, 44 | //! ...> :binary, 45 | //! ...> :exit_status 46 | //! ...> ]) 47 | //! #Port<0.1444> 48 | //! iex> Port.command(port, :erlang.term_to_binary("hello")) 49 | //! true 50 | //! iex> receive do 51 | //! ...> {^port, {:data, binary}} -> 52 | //! ...> IO.puts(:erlang.binary_to_term(binary)) 53 | //! ...> end 54 | //! "HELLO" 55 | //! :ok 56 | //! ``` 57 | //! 58 | //! If you wish to implement a line-based port or a custom port protocol (using 59 | //! the :stream option) you can do so by implementing the 60 | //! `PortSend`/`PortReceive` traits. 61 | 62 | extern crate byteorder; 63 | extern crate serde; 64 | extern crate serde_eetf; 65 | 66 | #[macro_use] 67 | extern crate serde_derive; 68 | 69 | use byteorder::BigEndian; 70 | use serde::de::DeserializeOwned; 71 | use serde::Serialize; 72 | use std::io::{Read, Write}; 73 | use std::marker::PhantomData; 74 | 75 | /// Creates an IOPort using stdin & stdout with the specified packet size. 76 | /// 77 | /// This should be used with ports that have been opened in Erlang/Elixir with 78 | /// the `:use_stdio` and `{:packet, N}` options, where N matches packet_size. 79 | /// 80 | /// Note: at present we don't do any locking of the underlying stdin or stdout 81 | /// streams, so you should only call this once in your port. This may be changed 82 | /// in the future... 83 | pub fn stdio( 84 | packet_size: PacketSize, 85 | ) -> IOPort, PacketSender> { 86 | IOPort { 87 | receiver: PacketReceiver::from_reader(std::io::stdin(), packet_size), 88 | sender: PacketSender::from_writer(std::io::stdout(), packet_size), 89 | } 90 | } 91 | 92 | /// Creates an IOPort using file descriptors 3 and 4 with the specified packet size. 93 | /// 94 | /// This allows the port application to continue to use stdout for logging/other 95 | /// output. 96 | /// 97 | /// This should be used with ports that have been opened in Erlang/Elixir with 98 | /// the `:nouse_stdio` and `{:packet, N}` options, where N matches packet_size. 99 | /// 100 | /// This function is unsafe, and if you call this function for a port that was 101 | /// not created with nouse_stdio then it will panic. 102 | pub unsafe fn nouse_stdio( 103 | packet_size: PacketSize, 104 | ) -> IOPort, PacketSender> { 105 | use std::fs::File; 106 | use std::os::unix::io::FromRawFd; 107 | 108 | IOPort { 109 | receiver: PacketReceiver::from_reader(File::from_raw_fd(3), packet_size), 110 | sender: PacketSender::from_writer(File::from_raw_fd(4), packet_size), 111 | } 112 | } 113 | 114 | /// `PortReceive` allows an app to receive messages through an Erlang port. 115 | pub trait PortReceive { 116 | /// Receives a single message over the port. 117 | /// 118 | /// If there are no more messages returns None. If there's a problem reading 119 | /// the message it will panic. 120 | /// 121 | /// A `MessageDeserialize` implementation shold exist for the message we are 122 | /// reading. We provide a `serde_eetf` based default implementation for any 123 | /// type implementing serdes `DeserializeOwned`. 124 | fn receive(&mut self) -> Option 125 | where 126 | T: MessageDeserialize; 127 | 128 | /// Creates an Iterator over a series of messages read from the port. 129 | fn iter<'a, T>(&'a mut self) -> MessageIterator<'a, Self, T> 130 | where 131 | Self: Sized, 132 | { 133 | MessageIterator::from_receiver(self) 134 | } 135 | } 136 | 137 | /// `PortSend` allows an application to send messages through an Erlang port. 138 | pub trait PortSend { 139 | /// Sends a single message over the port. 140 | /// 141 | /// A `MessageSerialize` implementation shold exist for the message we are 142 | /// reading. We provide a `serde_eetf` based default implementation for any 143 | /// type implementing serdes `Serialize`. 144 | /// 145 | /// In Erlang "Let it Crash" style, if we fail to encode the command or write it 146 | /// to a stream we will panic. Since this library is intended to be used as part 147 | /// of an Erlang system this should be picked up by the BEAM VM which can 148 | /// restart the Port. 149 | /// 150 | /// It's possible that panicing is _not_ what you want, for example if you have 151 | /// a Port that is handling multiple commands concurrently. Feel free to make a 152 | /// PR to better support your use case if that is the case. 153 | fn send(&mut self, data: T) 154 | where 155 | T: MessageSerialize; 156 | 157 | /// Sends an `{:ok, T}` or `{:error, E}` over the port. 158 | /// 159 | /// This can be used to reply to a command that was received in the port, or to 160 | /// send arbitrary commands to the Port inside the BEAM. 161 | /// 162 | /// Applications can pass any data type they want into `reply`, provided there's 163 | /// a definition of `Into>` for that type. A default 164 | /// implementation is provided for `Result`. 165 | /// 166 | /// Note that both E and T must implement `Serialize` rather than 167 | /// `MessageSerialize` as is the case for `send`. 168 | /// 169 | /// If you wish to return a custom type you can implement `Into>` for that type, or use the `send` function instead to send a custom 171 | /// type. 172 | /// 173 | /// At the moment this function is just a simple wrapper around `send` but that 174 | /// may change in the future. 175 | /// 176 | /// In Erlang "Let it Crash" style, if we fail to encode the command or write it 177 | /// to a stream we will panic. Since this library is intended to be used as part 178 | /// of an Erlang system this should be picked up by the BEAM VM which can 179 | /// restart the Port. 180 | /// 181 | /// It's possible that panicing is _not_ what you want, for example if you have 182 | /// a Port that is handling multiple commands concurrently. Feel free to make a 183 | /// PR to better support your use case if that is the case. 184 | fn reply(&mut self, response: R) 185 | where 186 | R: Into>, 187 | T: Serialize, 188 | E: Serialize, 189 | { 190 | self.send::>(response.into()); 191 | } 192 | } 193 | 194 | /// The size of a packet as sent/received by a PacketReceiver or PacketSender 195 | /// 196 | /// You should pick the PacketSize that corresponds to the `{:packet, N}` option 197 | /// you are opening the port with in Erlang/Elixir. 198 | #[derive(Clone, Copy)] 199 | pub enum PacketSize { 200 | One, 201 | Two, 202 | Four, 203 | } 204 | 205 | /// A receiver for ports opened in Packet mode. 206 | /// 207 | /// If a port is opened with the `{:packet, N}` option then this receiver can 208 | /// be used with the `packet_size` set to `N` 209 | pub struct PacketReceiver 210 | where 211 | R: Read, 212 | { 213 | reader: R, 214 | packet_size: PacketSize, 215 | } 216 | 217 | impl PacketReceiver 218 | where 219 | R: Read, 220 | { 221 | pub fn from_reader(reader: R, packet_size: PacketSize) -> Self { 222 | PacketReceiver { 223 | reader: reader, 224 | packet_size: packet_size, 225 | } 226 | } 227 | 228 | fn read_size(&mut self) -> Result { 229 | use byteorder::ReadBytesExt; 230 | 231 | match self.packet_size { 232 | PacketSize::One => self.reader.read_u8().map(|n| n as usize), 233 | PacketSize::Two => self.reader.read_u16::().map(|n| n as usize), 234 | PacketSize::Four => self.reader.read_u32::().map(|n| n as usize), 235 | } 236 | } 237 | } 238 | 239 | impl PortReceive for PacketReceiver 240 | where 241 | R: Read, 242 | { 243 | fn receive(&mut self) -> Option 244 | where 245 | T: MessageDeserialize, 246 | { 247 | match self.read_size() { 248 | Ok(packet_size) => { 249 | let mut buf = vec![0; packet_size]; 250 | self.reader 251 | .read_exact(&mut buf) 252 | .expect("Couldn't read full packet of data"); 253 | Some(MessageDeserialize::deserialize_message(buf)) 254 | } 255 | Err(err) => { 256 | if err.kind() == std::io::ErrorKind::UnexpectedEof { 257 | return None; 258 | } 259 | panic!("IO when reading size {}", err); 260 | } 261 | } 262 | } 263 | } 264 | 265 | /// A sender for ports opened in Packet mode. 266 | /// 267 | /// If a port is opened with the `{:packet, N}` option then this sender can 268 | /// be used with the `packet_size` set to `N` 269 | pub struct PacketSender 270 | where 271 | W: Write, 272 | { 273 | writer: W, 274 | packet_size: PacketSize, 275 | } 276 | 277 | impl PacketSender 278 | where 279 | W: Write, 280 | { 281 | pub fn from_writer(writer: W, packet_size: PacketSize) -> Self { 282 | PacketSender { 283 | writer: writer, 284 | packet_size: packet_size, 285 | } 286 | } 287 | 288 | fn write_size(&mut self, size: usize) -> Result<(), std::io::Error> { 289 | use byteorder::WriteBytesExt; 290 | 291 | // TODO: Should probably verify size fits within packet_size here... 292 | 293 | match self.packet_size { 294 | PacketSize::One => self.writer.write_u8(size as u8), 295 | PacketSize::Two => self.writer.write_u16::(size as u16), 296 | PacketSize::Four => self.writer.write_u32::(size as u32), 297 | } 298 | } 299 | } 300 | 301 | impl PortSend for PacketSender 302 | where 303 | W: Write, 304 | { 305 | fn send(&mut self, message: T) 306 | where 307 | T: MessageSerialize, 308 | { 309 | let data = message.serialize_message(); 310 | 311 | self.write_size(data.len()).expect("write data size failed"); 312 | self.writer.write_all(&data).expect("writing result failed"); 313 | self.writer.flush().expect("flushing stdout failed"); 314 | } 315 | } 316 | 317 | /// A wrapper around a receiver and a sender. 318 | /// 319 | /// This struct does not implement PortSend or PortReceive itself, as you could 320 | /// end up mutably borrowing the IOPort when calling `port.iter()` and then 321 | /// you'd be unable to call `port.reply` inside your loop. Instead you should 322 | /// call the functions on receiver and sender directly. This may be changed in a 323 | /// future release if I figure out a way to do it nicely. 324 | pub struct IOPort 325 | where 326 | R: PortReceive, 327 | S: PortSend, 328 | { 329 | pub receiver: R, 330 | pub sender: S, 331 | } 332 | 333 | /// Iterator over messages from a PortReceive. 334 | pub struct MessageIterator<'a, R: 'a, T> 335 | where 336 | R: PortReceive, 337 | { 338 | receiver: &'a mut R, 339 | phantom: PhantomData, 340 | } 341 | 342 | impl<'a, R, T> MessageIterator<'a, R, T> 343 | where 344 | R: PortReceive, 345 | { 346 | pub fn from_receiver(receiver: &'a mut R) -> MessageIterator<'a, R, T> { 347 | MessageIterator { 348 | receiver: receiver, 349 | phantom: PhantomData, 350 | } 351 | } 352 | } 353 | 354 | impl<'a, R, T> Iterator for MessageIterator<'a, R, T> 355 | where 356 | R: PortReceive, 357 | T: MessageDeserialize, 358 | { 359 | type Item = T; 360 | 361 | fn next(&mut self) -> Option { 362 | self.receiver.receive() 363 | } 364 | } 365 | 366 | /// Trait that parses some data from a Vec 367 | /// 368 | /// This is used in the receive function to deserialize commands. A default 369 | /// implementation is provided for anything implementing DeserializeOwned from 370 | /// serde. 371 | /// 372 | /// In Erlang "Let it Crash" style, if the data in `buffer` is malformed this 373 | /// trait shoul panic. Since this library is intended to be used as part of an 374 | /// Erlang system this should be picked up by the BEAM VM which can restart the 375 | /// Port. 376 | /// 377 | /// It's possible that panicing is _not_ what you want, for example if you have 378 | /// a Port that is handling multiple commands concurrently. Feel free to make a 379 | /// PR or raise an issue to better support your use case if that is the case. 380 | pub trait MessageDeserialize { 381 | fn deserialize_message(buffer: Vec) -> Self; 382 | } 383 | 384 | impl MessageDeserialize for T 385 | where 386 | T: DeserializeOwned, 387 | { 388 | fn deserialize_message(buffer: Vec) -> Self { 389 | serde_eetf::from_bytes(&buffer).expect("Deserialization Failed") 390 | } 391 | } 392 | 393 | /// Trait that serializes some data into a Vec 394 | /// 395 | /// This is used in the send function to serialize commands. A default 396 | /// implementation is provided for anything implementing Serialize from 397 | /// serde. 398 | /// 399 | /// In Erlang "Let it Crash" style, if we fail to serialize for whatever reason 400 | /// trait shoul panic. Since this library is intended to be used as part of an 401 | /// Erlang system this should be picked up by the BEAM VM which can restart the 402 | /// Port. 403 | /// 404 | /// It's possible that panicing is _not_ what you want, for example if you have 405 | /// a Port that is handling multiple commands concurrently. Feel free to make a 406 | /// PR or raise an issue to better support your use case if that is the case. 407 | pub trait MessageSerialize { 408 | fn serialize_message(self) -> Vec; 409 | } 410 | 411 | impl MessageSerialize for T 412 | where 413 | T: Serialize, 414 | { 415 | fn serialize_message(self: Self) -> Vec { 416 | serde_eetf::to_bytes(&self).expect("Serialization failed") 417 | } 418 | } 419 | 420 | /// A result enum for replying to commands from Erlang. 421 | /// 422 | /// This will serialize into a standard erlang result tuple of either: 423 | /// 424 | /// 1. `{:ok, result}` on Ok 425 | /// 2. `{:error err}` on Error 426 | /// 427 | /// All replies sent via `reply` are converted into this enum. 428 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 429 | pub enum ErlResult { 430 | Ok(T), 431 | Error(E), 432 | } 433 | 434 | impl From> for ErlResult { 435 | fn from(result: Result) -> Self { 436 | match result { 437 | Ok(success) => ErlResult::Ok(success), 438 | Err(error) => ErlResult::Error(error), 439 | } 440 | } 441 | } 442 | 443 | #[cfg(test)] 444 | mod tests { 445 | use super::*; 446 | 447 | use byteorder::WriteBytesExt; 448 | use serde_eetf; 449 | use std::io::Cursor; 450 | 451 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 452 | struct TestCommand { 453 | int8: i8, 454 | } 455 | 456 | #[test] 457 | fn test_packet_receiver_iter() { 458 | let buff = serde_eetf::to_bytes(&TestCommand { int8: 100 }).unwrap(); 459 | 460 | let mut size_buff = Vec::new(); 461 | size_buff 462 | .write_u32::(buff.len() as u32) 463 | .expect("write data size failed"); 464 | size_buff.extend_from_slice(&buff); 465 | size_buff 466 | .write_u32::(buff.len() as u32) 467 | .expect("write data size failed"); 468 | size_buff.extend_from_slice(&buff); 469 | 470 | let results: Vec = 471 | PacketReceiver::from_reader(&mut Cursor::new(size_buff), PacketSize::Four) 472 | .iter() 473 | .collect(); 474 | assert_eq!(results.len(), 2); 475 | assert_eq!(results[0], results[1]); 476 | } 477 | 478 | #[test] 479 | fn test_packet_sender_reply() { 480 | fn do_test(input: Result, expected_output: ErlResult) { 481 | let mut cursor = Cursor::new(vec![]); 482 | { 483 | let mut sender = PacketSender::from_writer(&mut cursor, PacketSize::Four); 484 | sender.reply(input); 485 | } 486 | cursor.set_position(0); 487 | 488 | let result: Vec> = 489 | PacketReceiver::from_reader(&mut cursor, PacketSize::Four) 490 | .iter() 491 | .collect(); 492 | 493 | assert_eq!(result, [expected_output]); 494 | } 495 | 496 | do_test(Ok(1), ErlResult::Ok(1)); 497 | do_test( 498 | Err("Nope".to_string()), 499 | ErlResult::Error("Nope".to_string()), 500 | ); 501 | } 502 | 503 | #[test] 504 | fn test_packet_sender_send() { 505 | fn do_test(packet_size: PacketSize) { 506 | let input = TestCommand { int8: 127 }; 507 | 508 | let mut cursor = Cursor::new(vec![]); 509 | { 510 | let mut sender = PacketSender::from_writer(&mut cursor, packet_size); 511 | sender.send(&input); 512 | } 513 | cursor.set_position(0); 514 | 515 | let result: Vec = PacketReceiver::from_reader(&mut cursor, packet_size) 516 | .iter() 517 | .collect(); 518 | 519 | assert_eq!(result, [input]); 520 | } 521 | 522 | do_test(PacketSize::One); 523 | do_test(PacketSize::Two); 524 | do_test(PacketSize::Four); 525 | } 526 | } 527 | --------------------------------------------------------------------------------