Skip to content

Commit

Permalink
Support for BinarySlice to avoid allocation
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiasBengtsson committed May 26, 2022
1 parent faa452e commit a36a366
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 0 deletions.
1 change: 1 addition & 0 deletions guide/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [`Vec`](./types/vec.md)
- [`HashMap`](./types/hashmap.md)
- [`Binary`](./types/binary.md)
- [`BinarySlice`](./types/binary_slice.md)
- [`Option`](./types/option.md)
- [Object](./types/object.md)
- [Class Object](./types/class_object.md)
Expand Down
50 changes: 50 additions & 0 deletions guide/src/types/binary_slice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# `Binary Slices`

Binary data is represented as a string in PHP. The most common source of this
data is from the [`pack`] and [`unpack`] functions. It allows you to use a PHP
string as a read-only slice in Rust.

| `T` parameter | `&T` parameter | `T` Return type | `&T` Return type | PHP representation |
| ------------- | -------------- | --------------- | ---------------- | ------------------ |
| Yes | No | No | No | `zend_string` |

The binary type is represented as a string in PHP. Although not encoded, the
data is converted into a slice and then the pointer to the data is set as the
string pointer, with the length of the array being the length of the string.

`BinarySlice<T>` is valid when `T` implements `PackSlice`. This is currently
implemented on most primitive numbers (i8, i16, i32, i64, u8, u16, u32, u64,
isize, usize, f32, f64).

[`pack`]: https://www.php.net/manual/en/function.pack.php
[`unpack`]: https://www.php.net/manual/en/function.unpack.php

## Rust Usage

```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::binary_slice::BinarySlice;
#[php_function]
pub fn test_binary_slice(input: BinarySlice<u8>) -> u8 {
let mut sum = 0;
for i in input.iter() {
sum += i;
}
sum
}
# fn main() {}
```

## PHP Usage

```php
<?php

$data = pack('C*', 1, 2, 3, 4, 5);
$output = test_binary_slice($data);
var_dump($output); // 15
```
2 changes: 2 additions & 0 deletions guide/src/types/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ have been implemented on most regular Rust types:
- `HashMap<String, T>` where T implements `IntoZval` and/or `FromZval`.
- `Binary<T>` where T implements `Pack`, used for transferring binary string
data.
- `BinarySlice<T>` where T implements `Pack`, used for exposing PHP binary
strings as read-only slices.
- A PHP callable closure or function wrapped with `Callable`.
- `Option<T>` where T implements `IntoZval` and/or `FromZval`, and where `None`
is converted to a PHP `null`.
Expand Down
148 changes: 148 additions & 0 deletions src/binary_slice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//! Provides implementations for converting from Zend binary strings as slices,
//! commonly returned from functions such as [`pack`] and [`unpack`].
//!
//! [`pack`]: https://www.php.net/manual/en/function.pack.php
//! [`unpack`]: https://www.php.net/manual/en/function.unpack.php
use crate::ffi::zend_string;

use std::{
convert::TryFrom,
ops::{Deref},
slice::from_raw_parts,
};

use crate::{
convert::{FromZval},
error::{Error, Result},
flags::DataType,
types::Zval,
};

/// Acts as a wrapper around [`&[T]`] where `T` implements [`PackSlice`].
/// Primarily used for passing read-only binary data into Rust functions.
#[derive(Debug)]
pub struct BinarySlice<'a, T>(&'a [T])
where T: PackSlice;

impl<'a, T> BinarySlice<'a, T>
where T: PackSlice {
/// Creates a new binary slice wrapper from a slice of data.
///
/// # Parameters
///
/// * `data` - Slice to store inside the binary wrapper.
pub fn new(data: &'a [T]) -> Self {
Self(data)
}
}

impl<'a, T> Deref for BinarySlice<'a, T>
where T: PackSlice {
type Target = &'a [T];

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<T> FromZval<'_> for BinarySlice<'_, T>
where T: PackSlice {
const TYPE: DataType = DataType::String;

fn from_zval(zval: &Zval) -> Option<Self> {
zval.binary_slice().map(BinarySlice)
}
}

impl<T> TryFrom<Zval> for BinarySlice<'_, T>
where T: PackSlice {
type Error = Error;

fn try_from(value: Zval) -> Result<Self> {
Self::from_zval(&value).ok_or_else(|| Error::ZvalConversion(value.get_type()))
}
}

impl<'a, T> From<BinarySlice<'a, T>> for &'a [T]
where T: PackSlice {
fn from(value: BinarySlice<'a, T>) -> Self {
value.0
}
}

impl<'a, T> From<&'a [T]> for BinarySlice<'a, T>
where T: PackSlice {
fn from(value: &'a [T]) -> Self {
Self::new(value)
}
}

/// Used to expose a Zend binary string as a slice. Useful in conjunction with
/// the [`pack`] and [`unpack`] functions built-in to PHP.
///
/// # Safety
///
/// The types cannot be ensured between PHP and Rust, as the data is represented
/// as a string when crossing the language boundary. Exercise caution when using
/// these functions.
///
/// [`pack`]: https://www.php.net/manual/en/function.pack.php
/// [`unpack`]: https://www.php.net/manual/en/function.unpack.php
pub unsafe trait PackSlice: Clone {
/// Creates a Rust slice from a given Zend binary string. Can be used to
/// pass data from `pack` in PHP to Rust without encoding into another
/// format. Note that the data *must* be all one type, as this
/// implementation only unpacks one type.
///
/// # Safety
///
/// There is no way to tell if the data stored in the string is actually of
/// the given type. The results of this function can also differ from
/// platform-to-platform due to the different representation of some
/// types on different platforms. Consult the [`pack`] function
/// documentation for more details.
///
/// # Parameters
///
/// * `s` - The Zend string containing the binary data.
///
/// [`pack`]: https://www.php.net/manual/en/function.pack.php
fn unpack_into<'a>(s: &zend_string) -> &'a [Self];
}

/// Implements the [`PackSlice`] trait for a given type.
macro_rules! pack_slice_impl {
($t: ty) => {
pack_slice_impl!($t, <$t>::BITS);
};

($t: ty, $d: expr) => {
unsafe impl PackSlice for $t {
fn unpack_into<'a>(s: &zend_string) -> &'a [Self] {
let bytes = ($d / 8) as usize;
let len = (s.len as usize) / bytes;
let ptr = s.val.as_ptr() as *const $t;
unsafe { from_raw_parts(ptr, len) }
}
}
};
}

pack_slice_impl!(u8);
pack_slice_impl!(i8);

pack_slice_impl!(u16);
pack_slice_impl!(i16);

pack_slice_impl!(u32);
pack_slice_impl!(i32);

pack_slice_impl!(u64);
pack_slice_impl!(i64);

pack_slice_impl!(isize);
pack_slice_impl!(usize);

pack_slice_impl!(f32, 32);
pack_slice_impl!(f64, 64);
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
pub mod alloc;
pub mod args;
pub mod binary;
pub mod binary_slice;
pub mod builders;
pub mod convert;
pub mod error;
Expand Down
10 changes: 10 additions & 0 deletions src/types/zval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::{convert::TryInto, ffi::c_void, fmt::Debug, ptr};

use crate::{
binary::Pack,
binary_slice::PackSlice,
boxed::ZBox,
convert::{FromZval, FromZvalMut, IntoZval, IntoZvalDyn},
error::{Error, Result},
Expand Down Expand Up @@ -137,6 +138,15 @@ impl Zval {
}
}

pub fn binary_slice<'a, T: PackSlice>(&self) -> Option<&'a [T]> {
if self.is_string() {
// SAFETY: Type is string therefore we are able to take a reference.
Some(T::unpack_into(unsafe { self.value.str_.as_ref() }?))
} else {
None
}
}

/// Returns the value of the zval if it is a resource.
pub fn resource(&self) -> Option<*mut zend_resource> {
// TODO: Can we improve this function? I haven't done much research into
Expand Down

0 comments on commit a36a366

Please sign in to comment.