Go to file
Chris Morgan 93511917c3 Rename UncheckedAnyExt, fix Extend, tweak things
The *name* UncheckedAnyExt was ending up visible in docs, but it had
become an increasingly unpleasant name. “Downcast” is suitable, though,
being private, it’s not still not perfect. But there’s no point in
making it public, as people generally can’t implement it because of
coherence rules (I tried).

Plus documentation and style changes.

As for Extend, eh, that should ideally be in a different commit, but
it’s here now, and I’m the only one working on this code base in
general, so I permit myself to be slightly lazy from time to time.
Trouble was Downcast should never have had an Any supertrait, as it was
grossly misleading, leading to type_id giving `dyn Any`’s TypeId rather
than the underlying type’s.
2022-02-03 01:07:00 +11:00
benches Remove the bench Cargo feature as superfluous 2017-07-07 10:55:35 +10:00
src Rename UncheckedAnyExt, fix Extend, tweak things 2022-02-03 01:07:00 +11:00
.gitignore Remove obsolete items from .gitignore 2022-01-26 00:16:15 +11:00
Cargo.toml 1.0.0-beta.1 2022-01-26 00:20:30 +11:00
CHANGELOG.md Rename UncheckedAnyExt, fix Extend, tweak things 2022-02-03 01:07:00 +11:00
COPYING Add the BlueOak-1.0.0 license 2022-01-26 00:16:15 +11:00
README.md Make README more approachable, give example 2022-01-26 00:16:15 +11:00
test chmod +x test 2022-01-26 01:09:17 +11:00
test-oldest-Cargo.lock no_std support 2022-01-26 00:16:15 +11:00

AnyMap, a safe and convenient store for one value of each type

AnyMap is a type-safe wrapper around HashMap<TypeId, Box<dyn Any>> that lets you not worry about TypeId or downcasting, but just get on with storing one each of a bag of diverse types, which is really useful for extensibility in some sorts of libraries.

Background

If youre familiar with Go and Go web frameworks, you may have come across the common “environment” pattern for storing data related to the request. Its typically something like map[string]interface{} and is accessed with arbitrary strings which may clash and type assertions which are a little unwieldy and must be used very carefully. (Personally I would consider that it is just asking for things to blow up in your face.) In a language like Go, lacking in generics, this is the best that can be done; such a thing cannot possibly be made safe without generics.

As another example of such an interface, JavaScript objects are exactly the same—a mapping of string keys to arbitrary values. (There it is actually more dangerous, because methods and fields/attributes/properties are on the same plane—though its possible to use Map these days.)

Fortunately, we can do better than these things in Rust. Our type system is quite equal to easy, robust expression of such problems.

Example

let mut data = anymap::AnyMap::new();
assert_eq!(data.get(), None::<&i32>);
data.insert(42i32);
assert_eq!(data.get(), Some(&42i32));
data.remove::<i32>();
assert_eq!(data.get::<i32>(), None);

#[derive(Clone, PartialEq, Debug)]
struct Foo {
    str: String,
}

assert_eq!(data.get::<Foo>(), None);
data.insert(Foo { str: format!("foo") });
assert_eq!(data.get(), Some(&Foo { str: format!("foo") }));
data.get_mut::<Foo>().map(|foo| foo.str.push('t'));
assert_eq!(&*data.get::<Foo>().unwrap().str, "foot");

Features

  • Store up to one value for each type in a bag.
  • Add Send or Send + Sync bounds.
  • You can opt into making the map Clone. (In theory you could add all kinds of other functionality, but you cant readily make this work generically, and the bones of it are simple enough that it becomes better to make your own extension of Any and reimplement AnyMap.)
  • no_std if you like.

Cargo features/dependencies/usage

Typical Cargo.toml usage:

[dependencies]
anymap = "1.0.0-beta.1"

No-std usage, using alloc and the hashbrown crate instead of std::collections::HashMap:

[dependencies]
anymap = { version = "1.0.0-beta.1", default-features = false, features = ["hashbrown"] }

The std feature is enabled by default. The hashbrown feature overrides it. At least one of the two must be enabled.

On stability: hashbrown is still pre-1.0.0 and experiencing breaking changes. Because its useful for a small fraction of users, I am retaining it, but with different compatibility guarantees to the typical SemVer ones. Where possible, I will just widen the range for new releases of hashbrown (e.g. change 0.12 to >=0.12, <0.14 when they release 0.13.0), but if an incompatible change occurs, I may drop support for older versions of hashbrown with a bump to the minor part of the anymap version number (e.g. 1.1.0, 1.2.0). Iff youre using this feature, this is cause to consider using a tilde requirement like "~1.0" (or spell it out as >=1, <1.1).

Unsafe code in this library

This library uses a fair bit of unsafe code for several reasons:

  • To support CloneAny, unsafe code is required (because the downcast methods are defined on dyn Any rather than being trait methods); if you wanted to ditch Clone support this unsafety could be removed.

  • In the interests of performance, skipping various checks that are unnecessary because of the invariants of the data structure (no need to check the type ID when its been statically ensured by being used as the hash map key).

  • In the Clone implementation of dyn CloneAny with Send and/or Sync auto traits added, an unsafe block is used where safe code used to be used in order to avoid a spurious future-compatibility lint.

Its not possible to remove all unsafety from this library without also removing some of the functionality. Still, at the cost of the CloneAny functionality and the raw interface, you can definitely remove all unsafe code. Heres how you could do it:

  • Remove the genericness of it all (choose dyn Any, dyn Any + Send or dyn Any + Send + Sync and stick with it);
  • Merge anymap::raw into the normal interface, flattening it;
  • Change things like .map(|any| unsafe { any.downcast_unchecked() }) to .and_then(|any| any.downcast()) (performance cost: one extra superfluous type ID comparison, indirect).

Yeah, the performance costs of going safe are quite small. The more serious matter is the loss of Clone.

But frankly, if you wanted to do all this itd be easier and faster to write it from scratch. The core of the library is actually really simple and perfectly safe, as can be seen in src/lib.rs in the first commit (note that that code wont run without a few syntactic alterations; it was from well before Rust 1.0 and has things like Any:'static where now we have Any + 'static).

Colophon

Authorship: Chris Morgan is the author and maintainer of this library.

Licensing: this library is distributed under the terms of the Blue Oak Model License 1.0.0, the MIT License and the Apache License, Version 2.0, at your choice. See COPYING for details.