-
-
Notifications
You must be signed in to change notification settings - Fork 148
Description
Currently, most string fields in the twilight-model crate take in an owned String. This is fine, but I realize that for most cases, a plain &'static str is sufficient. The mandatory heap allocation (from the String) is thus usually unnecessary. Consider this example with rich embeds (using 0.10.2):
use twilight_model::channel::embed::{Embed, EmbedField};
let embed = Embed {
// Most fields omitted for brevity...
title: Some(String::from("A super cool rich embed!")),
description: Some(String::from("Some super cool description, too...")),
kind: String::from("rich"),
fields: Vec::from([EmbedField {
name: String::from("`help`"),
value: String::from("This is the help command."),
inline: false,
}]),
};Hopefully, it is apparent from my example that there are too many String allocations here, where plain &'static str slices will suffice. We can also note the Vec allocation, which is arguably unnecessary since we know for certain that there is only one element.1
Of course, this example also applies to other areas of the library. Rich embeds are just one particular instance.
A Possible Solution
In line with the goal to reduce heap allocations, I propose that we consider using the alloc::borrow::Cow smart pointer instead of String and Vec. The Cow smart pointer employs "copy-on-write" semantics so that heap allocations are kept at a minimum.
Now, I understand that ownership is one of the primary reasons why String and Vec were chosen. After all, it becomes rather cumbersome to use many of the data models if explicit lifetimes were required—though, I personally wouldn't be totally against this proposition.
Anyway, a good compromise is to use a Cow<'static, str> and a Cow<'static, [T]> (for all types T). Observe that the 'static lifetime specifier enforces that the data model (practically) "owns" the slice, even though it is technically borrowed. Therefore, the enclosing struct need not be generic over arbitrary lifetimes.
use alloc::borrow::Cow;
struct User {
/// The `'static` lifetime allows us to treat this `struct`
/// _as if_ it owns a string. There is no need to have the
/// `User` be generic over arbitrary lifetimes. Hooray! 🥳
name: Cow<'static, str>,
bytes: Cow<'static, [u8]>,
}This gives the user extra flexibility on whether to use a &'static str or a regular String (e.g. in cases where string interpolation is necessary). Consider the same rich embed example from earlier:
use alloc::borrow::Cow;
use twilight_model::channel::embed::{Embed, EmbedField};
let embed = Embed {
// Most fields omitted for brevity...
title: Some(Cow::Borrowed("A super cool rich embed!")),
description: Some(Cow::Borrowed("Some super cool description, too...")),
kind: Cow::Borrowed("rich"),
fields: Cow::Borrowed(&[EmbedField {
name: Cow::Borrowed("`help`"),
value: Cow::Borrowed("This is the help command."),
inline: false,
}]),
};It is important to note that there are now zero allocations here. Both the Vec and String have been removed in favor of their 'static counterparts. In case string interpolation or dynamic arrays are needed, one may use Cow::Owned variant instead (containing the appropriate String or Vec). Consider this other example for mixed usage:
use alloc::borrow::Cow;
use twilight_model::channel::embed::{Embed, EmbedField};
// Example dynamic data
let name: Box<str> = get_username_from_somewhere();
let num: u32 = get_number_from_somewhere();
let embed = Embed {
// Most fields omitted for brevity...
title: Some(Cow::Owned(format!("Your number is: {num}."))),
description: Some(Cow::Borrowed("Some static description, too...")),
kind: Cow::Borrowed("rich"),
fields: Cow::Borrowed(&[EmbedField {
name: Cow::Borrowed("`help`"),
value: Cow::Owned(format!("Hi, {name}. This is the help command.")),
inline: false,
}]),
};Suggestions for Migration
Most builder setters and utilities from this library rely on the fact that the intended target field is a String or a Vec. I'm aware that it will be rather tedious to convert all of the parameters to their Cow-equivalent. Luckily, generics come to the rescue! Suppose we intend to migrate to the Cow smart pointer for the builders in the twilight-util crate:
+ // We may consider using type aliases for convenience.
+ type DiscordStr = alloc::borrow::Cow<'static, str>;
impl EmbedFieldBuilder {
- pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
+ pub fn new(name: impl Into<DiscordStr>, value: impl Into<DiscordStr>) -> Self {
todo!()
}
}The standard library already provides the convenience From and Into implementations. From the user's perspective, constructing an EmbedFieldBuilder still works the same as before thanks to the standard conversions.
// The generics should evaluate to the `Cow::<'static, str>::Borrowed` variant.
let field = EmbedFieldBuilder::new("`help`", "This is the help command.");Footnotes
-
In addition to the heap allocation, extra copies of the data also occur due to the fact that the
String::fromimplementation simply copies the bytes from the&'static strinto the new allocation. TheVecworks in a similar fashion. ↩