Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hybrid-array: Added serde impls for Array #979

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions hybrid-array/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Pending

- Added `serde` feature - implements `Serialize` and `Deserialize` for `Array`
- Added `ArrayExt::try_from_fn`

## 0.1.0 (2022-05-07)
- Initial release
9 changes: 9 additions & 0 deletions hybrid-array/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ rust-version = "1.65"

[dependencies]
typenum = "1.17"
serde = { version = "1", optional = true }

[dev-dependencies]
bincode = "1.3"
serde_json = "1"

[features]
default = []
serde = ["dep:serde"]
23 changes: 23 additions & 0 deletions hybrid-array/PERMISSIONS
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
generic-array

The MIT License (MIT)

Copyright (c) 2015 Bartłomiej Kamiński

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
4 changes: 4 additions & 0 deletions hybrid-array/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.

### Permissions

Permissions and notices for code we use or modify can be found in [PERMISSIONS](PERMISSIONS).

[//]: # (badges)

[crate-image]: https://buildstats.info/crate/hybrid-array
Expand Down
139 changes: 139 additions & 0 deletions hybrid-array/src/impl_serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// This file was modified from
// https://github.com/fizyk20/generic-array/blob/0e2a03714b05bb7a737a677f8df77d6360d19c99/src/impl_serde.rs

use crate::{Array, ArraySize};
use core::{fmt, marker::PhantomData};
use serde::{
de::{self, SeqAccess, Visitor},
ser::SerializeTuple,
Deserialize, Deserializer, Serialize, Serializer,
};

impl<T, N: ArraySize> Serialize for Array<T, N>
where
T: Serialize,
{
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut tup = serializer.serialize_tuple(N::USIZE)?;
for el in self {
tup.serialize_element(el)?;
}

tup.end()
}
}

struct ArrayVisitor<T, N> {
_t: PhantomData<T>,
_n: PhantomData<N>,
}

// to avoid extra computation when testing for extra elements in the sequence
struct Dummy;
impl<'de> Deserialize<'de> for Dummy {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(Dummy)
}
}

impl<'de, T, N: ArraySize> Visitor<'de> for ArrayVisitor<T, N>
where
T: Deserialize<'de>,
{
type Value = Array<T, N>;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "struct Array<T, U{}>", N::USIZE)
}

fn visit_seq<A>(self, mut seq: A) -> Result<Array<T, N>, A::Error>
where
A: SeqAccess<'de>,
{
// Check the length in advance
match seq.size_hint() {
Some(n) if n != N::USIZE => {
return Err(de::Error::invalid_length(n, &self));
}
_ => {}
}

// Deserialize the array
let arr = Array::try_from_fn(|idx| {
let next_elem_opt = seq.next_element()?;
next_elem_opt.ok_or(de::Error::invalid_length(idx, &self))
});

// If there's a value allegedly remaining, and deserializing it doesn't fail, then that's a
// length mismatch error
if seq.size_hint() != Some(0) && seq.next_element::<Dummy>()?.is_some() {
// The addition only saturates if this array is the size of all addressable memory. Not
// going to happen. And if it did, the only effect is an off-by-one error message
Err(de::Error::invalid_length(N::USIZE.saturating_add(1), &self))
} else {
arr
}
}
}

impl<'de, T, N: ArraySize> Deserialize<'de> for Array<T, N>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Array<T, N>, D::Error>
where
D: Deserializer<'de>,
{
let visitor = ArrayVisitor {
_t: PhantomData,
_n: PhantomData,
};
deserializer.deserialize_tuple(N::USIZE, visitor)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_serialize() {
let array = Array::<u8, typenum::U2>::default();
let serialized = bincode::serialize(&array);
assert!(serialized.is_ok());
}

#[test]
fn test_deserialize() {
let mut array = Array::<u8, typenum::U2>::default();
array[0] = 1;
array[1] = 2;
let serialized = bincode::serialize(&array).unwrap();
let deserialized = bincode::deserialize::<Array<u8, typenum::U2>>(&serialized);
assert!(deserialized.is_ok());
let array = deserialized.unwrap();
assert_eq!(array[0], 1);
assert_eq!(array[1], 2);
}

#[test]
fn test_serialized_size() {
let array = Array::<u8, typenum::U1>::default();
let size = bincode::serialized_size(&array).unwrap();
assert_eq!(size, 1);
}

#[test]
#[should_panic]
fn test_too_many() {
let serialized = "[1, 2, 3, 4, 5]";
let _ = serde_json::from_str::<Array<u8, typenum::U4>>(serialized).unwrap();
}
}
73 changes: 71 additions & 2 deletions hybrid-array/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ use typenum::{Diff, Sum, Unsigned};

mod impls;

#[cfg(feature = "serde")]
mod impl_serde;

/// Hybrid typenum-based and const generic array type.
///
/// Provides the flexibility of typenum-based expressions while also
Expand All @@ -64,6 +67,14 @@ where
Self(ArrayExt::from_fn(cb))
}

/// Create array where each array element `T` is returned by the `cb` call.
pub fn try_from_fn<F, E>(cb: F) -> Result<Self, E>
where
F: FnMut(usize) -> Result<T, E>,
{
ArrayExt::try_from_fn(cb).map(Self)
}

/// Create array from a slice.
pub fn from_slice(slice: &[T]) -> Result<Self, TryFromSliceError>
where
Expand Down Expand Up @@ -565,10 +576,23 @@ pub trait ArrayOps<T, const N: usize>:

/// Extension trait with helper functions for core arrays.
pub trait ArrayExt<T>: Sized {
/// Try to create an array using the given callback function for each element. Returns an error
/// if any one of the calls errors
fn try_from_fn<F, E>(cb: F) -> Result<Self, E>
where
F: FnMut(usize) -> Result<T, E>;

/// Create array using the given callback function for each element.
fn from_fn<F>(cb: F) -> Self
#[allow(clippy::unwrap_used, unused_qualifications)]
fn from_fn<F>(mut cb: F) -> Self
where
F: FnMut(usize) -> T;
F: FnMut(usize) -> T,
{
// Turn the ordinary callback into a Result callback that always returns Ok
let wrapped_cb = |idx| Result::<T, ::core::convert::Infallible>::Ok(cb(idx));
// Now use the try_from version of this method
Self::try_from_fn(wrapped_cb).unwrap()
}

/// Create array from a slice, returning [`TryFromSliceError`] if the slice
/// length does not match the array length.
Expand All @@ -578,6 +602,9 @@ pub trait ArrayExt<T>: Sized {
}

impl<T, const N: usize> ArrayExt<T> for [T; N] {
// TODO: Eventually remove this and just use the default implementation. It's still
// here because the current Self::try_from_fn might be doing multiple passes over the data,
// depending on optimizations. Thus, this may be faster.
fn from_fn<F>(mut cb: F) -> Self
where
F: FnMut(usize) -> T,
Expand All @@ -591,6 +618,48 @@ impl<T, const N: usize> ArrayExt<T> for [T; N] {
})
}

fn try_from_fn<F, E>(mut cb: F) -> Result<Self, E>
where
F: FnMut(usize) -> Result<T, E>,
{
// TODO: Replace this entire function with array::try_map once it stabilizes
// https://doc.rust-lang.org/std/primitive.array.html#method.try_map

// Make an uninitialized array. We will populate it element-by-element
let mut arr: [MaybeUninit<T>; N] = unsafe { MaybeUninit::uninit().assume_init() };

// Run the callbacks. On success, write it to the array. On error, drop the values we've
// allocated already, then return the error. Note: dropping a `MaybeUninit` does nothing,
// so if there is a panic during this loop, we have a memory leak, but there is no memory
// safety issue.
let mut err_info = None;
for (idx, elem) in arr.iter_mut().enumerate() {
match cb(idx) {
Ok(val) => {
elem.write(val);
}
Err(err) => {
err_info = Some((idx, err));
}
}
}

// If an error occurred, go back and drop all the initialized values. Otherwise the values
// leak.
if let Some((err_idx, err)) = err_info {
arr.iter_mut()
.take(err_idx)
.for_each(|v| unsafe { v.assume_init_drop() });
return Err(err);
}

// If we've made it this far, all the elements have been written. Convert the uninitialized
// array to an initialized array
// TODO: Replace this map with MaybeUninit::array_assume_init() once it stabilizes
let arr = arr.map(|v: MaybeUninit<T>| unsafe { v.assume_init() });
Ok(arr)
}

fn from_slice(slice: &[T]) -> Result<Self, TryFromSliceError>
where
T: Copy,
Expand Down
Loading