#type #box #dynamic #static #data-type #no-std

no-std bin+lib unibox

Universal Box that can store any type using static or dynamic memory

2 unstable releases

0.2.0 Nov 26, 2021
0.1.0 Nov 20, 2021

#2522 in Rust patterns

MIT license

33KB
638 lines

UniBox

Universal Box.

Usually, when we want to store different types in a collection we use either one of the following techniques:

  1. An enum to wrap all possible types.
  2. A boxed trait that all types must implement.

Sometimes none of the above techniques are possible. The set of types might be unknown beforehand, for example in a library. And, when we implement a trait, we can only access the methods defined in the trait, not all the methods and properties of the original struct. Furthermore, we are required to use a Box, that means allocating dynamic memory, making no_std applications more complicated.

If you encountered any of these limitations, UniBox is probably a viable solution for your use case.

UniBox can:

  • Store a generic type without using generics in the struct signature.
  • Use either static or dynamic memory, you decide.
  • Return a reference to any type.
  • Be used to store mixed data in collections or arrays.

UniBox offers two kinds of types:

  • Static: uniboxes that store data without using the heap. They have a fixed size, and the type they host can't be bigger than that. Currently there are four types: UniBox32, UniBox64, UniBox128 and UniBox256, to store types up to 32, 64, 128 and 256 bytes. All these types are based on the generic static type, UniBoxN, that can also be used to implement custom static uniboxes.
  • Dynamic: store data by allocating memory, like a regular Box. There is only one type, UniBox.

Usage

Suppose we have 2 different structs with little or nothing in common, like the following User and Server. And we want to store instances of these types in the same collection.

We can use static uniboxes like so:

use unibox::{ Uniboxed, UniBox64 };

#[derive(Debug)]
struct BornDate {
    pub year: u16,
    pub month: u8,
    pub day: u8,
}

#[derive(Debug)]
struct User {
    pub name: String,
    pub lastname: String,
    pub born: BornDate,
}

#[derive(Debug)]
struct Server {
    pub domain: String,
    pub port: u16
}

let ubox_usr = UniBox64::new(
    User {
        name: "John".to_owned(),
        lastname: "Dow".to_owned(),
        born: BornDate {
            year: 1984,
            month: 12,
            day: 25
        }
    }
).expect("Couldn't create UniBox64 for User");

let ubox_server = UniBox64::new(
    Server {
        domain: "example.com".to_owned(),
        port: 8080
    }
).expect("Couldn't create UniBox64 for Server");

// Create a vector with the uniboxes
let v = vec!(ubox_usr, ubox_server);

for ubox in v.iter() {
    match ubox.id() {
        "my_crate::User" => {
            // It's a User struct
            println!("{:#?}", unsafe { ubox.as_ref::<User>() });
        },
        "my_crate::Server" => {
            // It's a Server struct
            println!("{:#?}", unsafe { ubox.as_ref::<Server>() });
        },
        _ => {}
    }
}

The dynamic version, UniBox, works exactly in the same way, the only difference is that it allocates memory to store the type and thus, you don't have to worry about the size.

Uniboxing types with references

Is possible to unibox a type that contains a reference with non-static lifetime, like so:

struct MyStruct<'a> {
    my_ref: &'a [i32]
}

let arr = [1, 2, 3, 4, 5];

let ubox = UniBox32::new(
    MyStruct {
        my_ref: &arr
    }
).expect("Failed uniboxing MyStruct");

println!("{:#?}", unsafe { ubox.as_ref::<MyStruct>() }.my_ref);

But once the type is embedded inside a UniBox, the rust compiler looses track of it, and it won't be able to ensure that lifetime constraints are observed. For this reason, is the programmer who must make sure that no references are used after being droped the original value. That's the main reason why Uniboxed::as_ref and Uniboxed::as_mut_ref are unsafe.

Why not Any?

The Any trait exposes a similar functionality, it allows a generic type to be casted, but it has some limitations compared to uniboxes:

  1. Only types with static references can be used, so something like the following can't be allocated inside a Box<dyn Any>:
struct MyStruct<'a> {
    my_ref: &'a [i32]
}
  1. The size of a dyn Any can't be known at compile time, and thus, we can't use it to store generic types inside arrays or other non-heap memory artifacts.

Features and no_std

This crate is no_std, but it uses the alloc crate to allocate dynamic memory inside UniBox. This is controlled via a feature, enabled by default, named alloc.

If your environment doesn't provide the alloc crate, just disable the default features. If you do so, you won't be able to use UniBox type.

No runtime deps

Features