Skip to content

Commit

Permalink
Add more detailed docs for binaries
Browse files Browse the repository at this point in the history
  • Loading branch information
JayKickliter committed Sep 18, 2020
1 parent 5d07755 commit e6c4437
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 50 deletions.
221 changes: 172 additions & 49 deletions rustler/src/types/binary.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,147 @@
use crate::wrapper::binary::{alloc, realloc, ErlNifBinary};
use crate::{Decoder, Encoder, Env, Error, NifResult, Term};
//! Safe wrappers around Erlang binaries.
//!
//! Rustler provides two binary types: [`Binary`] and [`OwnedBinary`]. Both
//! represent a contiguous region `u8`s, and they both use the Erlang allocator. The
//! primary difference between the two is their ownership semantics.
//!
//! The _owned_ in `OwnedBinary` refers to the fact that it owns the binary it
//! wraps. The _owner_ of an `OwnedBinary` is free to modify its contents. Ownership
//! lasts until it is dropped or consumed by converting it into a regular
//! `Binary`. An `OwnedBinary` can not be copied or cloned and is thus always moved.
//!
//! The `Binary` type is an immutable shared-reference to a binary. `Binary`s are
//! cheap to copy: all copies of a `Binary` point to the original `Binary`'s
//! data. Additionally, a `Binary`'s lifetime is tied to that of the NIF's [`Env`],
//! preventing outstanding references to the data after a NIF returns.
//!
//! # Examples
//!
//! Constructing an `OwnedBinary`:
//!
//! ```no_run
//! # use rustler::OwnedBinary;
//! {
//! let mut bin = OwnedBinary::new(5).expect("allocation failed");
//! bin.as_mut_slice().copy_from_slice("hello".as_bytes());
//! } // <- `bin` is dropped here
//! ```
//!
//! The following NIF takes a binary as its only parameter and returns a new binary
//! where each element is exclusive-or'ed with a constant:
//!
//! ```no_run
//! # use rustler::{Env, OwnedBinary, Binary, NifResult, Error};
//! #[rustler::nif]
//! fn xor_example<'a>(env: Env<'a>, bin: Binary<'a>) -> NifResult<Binary<'a>> {
//! let mut owned: OwnedBinary = bin.to_owned().ok_or(Error::Term(Box::new("no mem")))?;
//! for byte in owned.as_mut_slice() {
//! *byte ^= 0xAA;
//! }
//!
//! // Ownership of `owned`'s data is transferred to `env` on the
//! // following line, so no additional heap allocations are incurred.
//! Ok(Binary::from_owned(owned, env))
//! }
//! ```
//!
//! The contents of a newly-allocated `OwnedBinary` is not initialized to any
//! particular value. If your usage of the binary requires the it's data to be
//! zeroed, for example, then you must explicit zero it. In this example, we
//! manually zeroize the binary before passing it as slice to a third party
//! function.
//!
//! ```no_run
//! # fn some_third_party_api(buf: &mut [u8]) {
//! # for elem in buf {
//! # if *elem == 0 { *elem = 1 } else { panic!("Not a zero!") }
//! # }
//! # }
//! # use rustler::{Env, OwnedBinary, Binary, NifResult, Error};
//! #[rustler::nif]
//! fn wrapper_for_some_<'a>(env: Env<'a>) -> NifResult<Binary<'a>> {
//! let mut owned = OwnedBinary::new(100).ok_or(Error::Term(Box::new("no mem")))?;
//! for byte in owned.as_mut_slice() {
//! *byte = 0;
//! }
//!
//! // Some third party API which requires the slice to be all zeros on entry.
//! some_third_party_api(owned.as_mut_slice());
//!
//! // The imaginary API call presumedly filled in our binary with meaningful
//! // data, so let's return it.
//! Ok(Binary::from_owned(owned, env))
//! }
//!
//! ```
//!
//! [`Binary`]: struct.Binary.html
//! [`Env`]: ../../env/struct.Env.html
//! [`OwnedBinary`]: struct.OwnedBinary.html
use std::borrow::{Borrow, BorrowMut};
use std::io::Write;
use std::mem::MaybeUninit;
use std::ops::{Deref, DerefMut};

// Owned
use crate::{
wrapper::binary::{alloc, realloc, ErlNifBinary},
Decoder, Encoder, Env, Error, NifResult, Term,
};
use std::{
borrow::{Borrow, BorrowMut},
io::Write,
mem::MaybeUninit,
ops::{Deref, DerefMut},
};

/// An mutable smart-pointer to an Erlang binary.
///
/// See [module-level doc](index.html) for more information.
pub struct OwnedBinary(ErlNifBinary);

impl<'a> OwnedBinary {
pub unsafe fn from_raw(inner: ErlNifBinary) -> OwnedBinary {
OwnedBinary(inner)
}

/// Allocates a new OwnedBinary with size `size`.
/// Allocates a new `OwnedBinary` with size `size`.
///
/// Memory is not be initialized. If uninitialized memory is undesirable, set it
/// manually.
///
/// Note that the memory is not initially guaranteed to be any particular value.
/// If an empty buffer is needed, you should manually zero it.
/// # Errors
///
/// If allocation fails, `None` is returned.
pub fn new(size: usize) -> Option<OwnedBinary> {
unsafe { alloc(size) }.map(OwnedBinary)
}

/// Copies a given `Binary`.
pub fn from_unowned(from_bin: &Binary) -> Option<OwnedBinary> {
let len = from_bin.len();
if let Some(mut bin) = OwnedBinary::new(len) {
if let Ok(write_len) = bin.as_mut_slice().write(from_bin.as_slice()) {
if write_len != len {
panic!("Could not copy binary");
}
Some(bin)
} else {
panic!("Could not copy binary");
}
} else {
None
}
/// Copies `src`'s data into a new `OwnedBinary`.
///
/// # Errors
///
/// If allocation fails, `None` is returned.
pub fn from_unowned(src: &Binary) -> Option<OwnedBinary> {
OwnedBinary::new(src.len()).map(|mut b| {
b.as_mut_slice().copy_from_slice(&src);
b
})
}

/// Attempts to reallocate the buffer with the new size.
/// Returns false if the buffer cannot be reallocated.
/// Attempts to reallocate `self` with the new size.
///
/// Memory outside the range of the original binary will not be initialized. If
/// uninitialized memory is undesirable, set it manually.
///
/// # Errors
///
/// If reallocation fails, `false` is returned. Data remains intact on error.
#[must_use]
pub fn realloc(&mut self, size: usize) -> bool {
unsafe { realloc(&mut self.0, size) }
}

/// Attempts to reallocate the buffer with the new size.
/// Attempts to reallocate `self` with the new size.
///
/// If reallocation fails, it will perform a copy instead.
/// Memory outside the range of the original buffer will
/// not be initialized. If this needs to be empty, clear it manually.
///
/// Memory outside the range of the original binary will not be initialized. If
/// uninitialized memory is undesirable, set it manually.
pub fn realloc_or_copy(&mut self, size: usize) {
if !self.realloc(size) {
let mut new = OwnedBinary::new(size).unwrap();
Expand All @@ -66,16 +156,22 @@ impl<'a> OwnedBinary {
}
}

pub fn as_slice(&self) -> &'a [u8] {
/// Extracts a slice containing the entire binary.
pub fn as_slice(&self) -> &[u8] {
unsafe { ::std::slice::from_raw_parts(self.0.data, self.0.size) }
}

pub fn as_mut_slice(&mut self) -> &'a mut [u8] {
/// Extracts a mutable slice of the entire binary.
pub fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { ::std::slice::from_raw_parts_mut(self.0.data, self.0.size) }
}

/// Releases control of the binary to the VM. After this point
/// the binary will be immutable.
/// Consumes `self` and returns an immutable `Binary`.
///
/// This method is the mirror of [`Binary::from_owned`], and they can be used
/// interchangeably.
///
/// [`Binary::from_owned`]: struct.Binary.html#method.from_owned
pub fn release(self, env: Env) -> Binary {
Binary::from_owned(self, env)
}
Expand Down Expand Up @@ -111,36 +207,50 @@ impl Drop for OwnedBinary {

unsafe impl Send for OwnedBinary {}

// Borrowed
/// An immutable smart-pointer to an Erlang binary.
///
/// See [module-level doc](index.html) for more information.
#[derive(Copy, Clone)]
pub struct Binary<'a> {
inner: ErlNifBinary,
term: Term<'a>,
}

impl<'a> Binary<'a> {
/// Transfers ownership of `bin` to the VM and returns an
/// immutable `Binary`.
pub fn from_owned(bin: OwnedBinary, env: Env<'a>) -> Self {
// We are transferring ownership of `bin`'s data to the
// VM. Therefore, we need to prevent `bin`'s destructor being
// called at the end of this scope. The least error-prone
// solution (compared to `mem::forget()`) is to wrap `bin` in
// a `ManuallyDrop` and EXPLICITLY NOT CALL `ManuallyDrop::drop()`.
let mut bin = std::mem::ManuallyDrop::new(bin);
/// Consumes `owned` and returns an immutable `Binary`.
pub fn from_owned(owned: OwnedBinary, env: Env<'a>) -> Self {
// We are transferring ownership of `owned`'s data to the
// environment. Therefore, we need to prevent `owned`'s destructor being
// called at the end of this scope. The least error-prone solution (compared
// to `mem::forget()`) is to wrap `owned` in a `ManuallyDrop` and EXPLICITLY
// NOT CALL `ManuallyDrop::drop()`.
let mut owned = std::mem::ManuallyDrop::new(owned);
let term = unsafe {
Term::new(
env,
rustler_sys::enif_make_binary(env.as_c_arg(), &mut bin.0),
rustler_sys::enif_make_binary(env.as_c_arg(), &mut owned.0),
)
};
Binary { inner: bin.0, term }
Binary {
inner: owned.0,
term,
}
}

/// Copies `self`'s data into a new `OwnedBinary`.
///
/// # Errors
///
/// If allocation fails, an error will be returned.
pub fn to_owned(&self) -> Option<OwnedBinary> {
OwnedBinary::from_unowned(self)
}

/// Creates a `Binary` from `term`.
///
/// # Errors
///
/// If `term` is not a binary, an error will be returned.
pub fn from_term(term: Term<'a>) -> Result<Self, Error> {
let mut binary = MaybeUninit::uninit();
if unsafe {
Expand All @@ -159,6 +269,11 @@ impl<'a> Binary<'a> {
})
}

/// Creates a `Binary` from `term`.
///
/// # Errors
///
/// If `term` is not an `iolist`, an error will be returned.
pub fn from_iolist(term: Term<'a>) -> Result<Self, Error> {
let mut binary = MaybeUninit::uninit();
if unsafe {
Expand All @@ -177,16 +292,24 @@ impl<'a> Binary<'a> {
})
}

/// Returns an Erlang term representation of `self`.
pub fn to_term<'b>(&self, env: Env<'b>) -> Term<'b> {
self.term.in_env(env)
}

/// Extracts a slice containing the entire binary.
pub fn as_slice(&self) -> &'a [u8] {
unsafe { ::std::slice::from_raw_parts(self.inner.data, self.inner.size) }
}

/// Returns a new view into the same binary.
/// This will not copy anything.
///
/// This method is analogous to subslicing (e.g. `some_data[offset..length]`) in
/// that it does not copy nor allocate data.
///
/// # Errors
///
/// If `offset + length` is out of bounds, an error will be returned.
pub fn make_subbinary(&self, offset: usize, length: usize) -> NifResult<Binary<'a>> {
let min_len = length.checked_add(offset);
if min_len.ok_or(Error::BadArg)? > self.inner.size {
Expand Down
1 change: 0 additions & 1 deletion rustler/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use crate::{Env, Error, NifResult, Term};
pub mod atom;
pub use crate::types::atom::Atom;

#[doc(hidden)]
pub mod binary;
pub use crate::types::binary::{Binary, OwnedBinary};

Expand Down

0 comments on commit e6c4437

Please sign in to comment.