safecast: Generic Casting for Rust

Haydn Vestal
CodeX
Published in
2 min readSep 12, 2024

--

One of the first problems we faced when developing TinyChain is the limitation of the built-in TryFrom and TryInto traits. These take ownership of the data to cast and only support a single error type, potentially requiring a clone (i.e. a memory allocation) for every possible type that a value might be cast into, even in the case that the cast fails. They also don’t specify any way to check in advance whether the cast is even possible.

Example

#[derive(Debug)]
struct Foo {
name: String,
number: i32,
}

struct Bar {
name: String,
number: u16,
}

impl TryFrom<Foo> for Bar {
type Error = num::TryFromIntError;


fn try_from(foo: Foo) -> Result<Self, Self::Error> {
let Foo { name, number } = foo;
number.try_into().map(|number| Bar { name, number })
}
}

enum Baz {
Foo(Foo),
Bar(Bar),
}

impl CastFrom<Bar> for Foo {
fn cast_from(bar: Bar) -> Self {
let Bar { name, number } = bar;

Foo {
name,
number: number as i32,
}
}
}

impl TryCastFrom<Foo> for Bar {
fn can_cast_from(foo: &Foo) -> bool {
foo.number >= 0 && foo.number <= u16::MAX as i32
}

fn opt_cast_from(foo: Foo) -> Option<Self> {
let Foo { name, number } = foo;


if number >= 0 && number <= u16::MAX as i32 {
Some(Self {
name,
number: number as u16,
})
} else {
None
}
}
}

as_type!(Baz, Foo, Foo);
as_type!(Baz, Bar, Bar);

Move only when needed

Consider this cast from Foo to Bar:

let name: String = "name".to_string();

let foo = Foo {
name: name.clone(),
number: -1,
};

// this is not ideal because it eats the value to be cast
assert!(Bar::try_from(foo).is_err());

We need a generic way to cast one type to another without allocating or panicking in case the cast fails, moving the value only when we know the cast will succeed.

// we can avoid this situation using safecast
let foo = Foo {
name: name.clone(),
number: 1,
};


let bar: Bar = if foo.matches::<Bar>() {
// by only casting after checking that the cast will succeed
foo.opt_cast_into().expect("bar")
} else {
unreachable!()
};

Enum type support

Safecast also provides an as_type macro to automate accessing enum values by type:

// the as_type macro implements helper methods for the Baz enum
let baz = Baz::from(bar);
assert_eq!(baz.as_type(), Option::<&Foo>::None);
assert_eq!(baz.as_type(), Some(&Bar { name, number: 1 }));

Custom error handling

Safecast also implements try_cast_from and try_cast_into methods to support context-dependent error types for casting failures.

assert!(Bar::try_cast_from(foo, |still_foo| format!("invalid Bar: {still_foo:?}")).is_err());

Get Involved

Version 0.2 is out now and we’re looking for volunteers to provide feedback on the API and improve our macro coverage. If this sounds like something you’re interested in, please let us know! You’re welcome to assign yourself an issue or submit a pull request with your code.

--

--