Skip to content

Commit 6d2ae48

Browse files
authored
Swap Command and related structs to avoid String allocs (#318)
1 parent 3a2ab5e commit 6d2ae48

File tree

16 files changed

+143
-115
lines changed

16 files changed

+143
-115
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020

2121
include:
2222
- name: MSRV
23-
toolchain: 1.74.0
23+
toolchain: 1.80.0
2424
# don't do doctests because they rely on new features for brevity
2525
# copy known Cargo.lock to avoid random dependency MSRV bumps to mess up our test
2626
command: cp .github/Cargo-msrv.lock Cargo.lock && cargo test --all-features --lib --tests

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ authors = ["kangalio <[email protected]>"]
33
edition = "2021"
44
name = "poise"
55
version = "0.6.1"
6-
rust-version = "1.74.0"
6+
rust-version = "1.80.0"
77
description = "A Discord bot framework for serenity"
88
license = "MIT"
99
repository = "https://github.com/serenity-rs/poise/"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Docs](https://img.shields.io/badge/docs-online-informational)](https://docs.rs/poise/)
44
[![Docs (git)](https://img.shields.io/badge/docs%20%28git%29-online-informational)](https://serenity-rs.github.io/poise/)
55
[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6-
[![Rust: 1.74+](https://img.shields.io/badge/rust-1.74+-93450a)](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html)
6+
[![Rust: 1.80+](https://img.shields.io/badge/rust-1.80+-93450a)](https://blog.rust-lang.org/2024/07/25/Rust-1.80.0.html)
77

88
# Poise
99
Poise is an opinionated Discord bot framework with a few distinctive features:

examples/fluent_localization/main.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use poise::serenity_prelude as serenity;
44
use translation::tr;
55

66
pub struct Data {
7-
translations: translation::Translations,
7+
translations: &'static translation::Translations,
88
}
99

1010
type Error = Box<dyn std::error::Error + Send + Sync>;
@@ -62,7 +62,12 @@ async fn main() {
6262

6363
let mut commands = vec![welcome(), info(), register()];
6464
let translations = translation::read_ftl().expect("failed to read translation files");
65-
translation::apply_translations(&translations, &mut commands);
65+
66+
// We leak the translations so we can easily copy around `&'static str`s, to the downside
67+
// that the OS will reclaim the memory at the end of `main` instead of the Drop implementation.
68+
let translations: &'static translation::Translations = Box::leak(Box::new(translations));
69+
70+
translation::apply_translations(translations, &mut commands);
6671

6772
let token = std::env::var("TOKEN").unwrap();
6873
let intents = serenity::GatewayIntents::non_privileged();

examples/fluent_localization/translation.rs

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Wraps the fluent API and provides easy to use functions and macros for translation
22
3+
use std::borrow::Cow;
4+
35
use crate::{Context, Data, Error};
46

57
type FluentBundle = fluent::bundle::FluentBundle<
@@ -30,27 +32,27 @@ pub(crate) use tr;
3032

3133
/// Given a language file and message identifier, returns the translation
3234
pub fn format(
33-
bundle: &FluentBundle,
35+
bundle: &'static FluentBundle,
3436
id: &str,
3537
attr: Option<&str>,
3638
args: Option<&fluent::FluentArgs<'_>>,
37-
) -> Option<String> {
39+
) -> Option<Cow<'static, str>> {
3840
let message = bundle.get_message(id)?;
3941
let pattern = match attr {
4042
Some(attribute) => message.get_attribute(attribute)?.value(),
4143
None => message.value()?,
4244
};
4345
let formatted = bundle.format_pattern(pattern, args, &mut vec![]);
44-
Some(formatted.into_owned())
46+
Some(formatted)
4547
}
4648

4749
/// Retrieves the appropriate language file depending on user locale and calls [`format`]
4850
pub fn get(
4951
ctx: Context,
50-
id: &str,
52+
id: &'static str,
5153
attr: Option<&str>,
5254
args: Option<&fluent::FluentArgs<'_>>,
53-
) -> String {
55+
) -> Cow<'static, str> {
5456
let translations = &ctx.data().translations;
5557
ctx.locale()
5658
// Try to get the language-specific translation
@@ -60,7 +62,7 @@ pub fn get(
6062
// If this message ID is not present in any translation files whatsoever
6163
.unwrap_or_else(|| {
6264
tracing::warn!("unknown fluent message identifier `{}`", id);
63-
id.to_string()
65+
Cow::Borrowed(id)
6466
})
6567
}
6668

@@ -97,7 +99,7 @@ pub fn read_ftl() -> Result<Translations, Error> {
9799

98100
/// Given a set of language files, fills in command strings and their localizations accordingly
99101
pub fn apply_translations(
100-
translations: &Translations,
102+
translations: &'static Translations,
101103
commands: &mut [poise::Command<Data, Error>],
102104
) {
103105
for command in &mut *commands {
@@ -108,21 +110,24 @@ pub fn apply_translations(
108110
Some(x) => x,
109111
None => continue, // no localization entry => skip localization
110112
};
111-
command
112-
.name_localizations
113-
.insert(locale.clone(), localized_command_name);
114-
command.description_localizations.insert(
113+
114+
let locale = Cow::Borrowed(locale.as_str());
115+
let name_localizations = command.name_localizations.to_mut();
116+
let description_localizations = command.description_localizations.to_mut();
117+
118+
name_localizations.push((locale.clone(), localized_command_name));
119+
description_localizations.push((
115120
locale.clone(),
116121
format(bundle, &command.name, Some("description"), None).unwrap(),
117-
);
122+
));
118123

119124
for parameter in &mut command.parameters {
120125
// Insert localized parameter name and description
121-
parameter.name_localizations.insert(
126+
parameter.name_localizations.to_mut().push((
122127
locale.clone(),
123128
format(bundle, &command.name, Some(&parameter.name), None).unwrap(),
124-
);
125-
parameter.description_localizations.insert(
129+
));
130+
parameter.description_localizations.to_mut().push((
126131
locale.clone(),
127132
format(
128133
bundle,
@@ -131,14 +136,14 @@ pub fn apply_translations(
131136
None,
132137
)
133138
.unwrap(),
134-
);
139+
));
135140

136141
// If this is a choice parameter, insert its localized variants
137-
for choice in &mut parameter.choices {
138-
choice.localizations.insert(
142+
for choice in parameter.choices.to_mut().iter_mut() {
143+
choice.localizations.to_mut().push((
139144
locale.clone(),
140145
format(bundle, &choice.name, None, None).unwrap(),
141-
);
146+
));
142147
}
143148
}
144149
}
@@ -170,7 +175,7 @@ pub fn apply_translations(
170175
);
171176

172177
// If this is a choice parameter, set the choice names to en-US
173-
for choice in &mut parameter.choices {
178+
for choice in parameter.choices.to_mut().iter_mut() {
174179
choice.name = format(bundle, &choice.name, None, None).unwrap();
175180
}
176181
}

macros/src/choice_parameter.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,16 @@ pub fn choice_parameter(input: syn::DeriveInput) -> Result<TokenStream, darling:
6767
let indices = 0..variant_idents.len();
6868
Ok(quote::quote! {
6969
impl poise::ChoiceParameter for #enum_ident {
70-
fn list() -> Vec<poise::CommandParameterChoice> {
71-
vec![ #( poise::CommandParameterChoice {
70+
fn list() -> std::borrow::Cow<'static, [poise::CommandParameterChoice]> {
71+
use ::std::borrow::Cow;
72+
73+
Cow::Borrowed(&[ #( poise::CommandParameterChoice {
7274
__non_exhaustive: (),
73-
name: #names.to_string(),
74-
localizations: std::collections::HashMap::from([
75-
#( (#locales.to_string(), #localized_names.to_string()) ),*
75+
name: Cow::Borrowed(#names),
76+
localizations: Cow::Borrowed(&[
77+
#( (Cow::Borrowed(#locales), Cow::Borrowed(#localized_names)) ),*
7678
]),
77-
}, )* ]
79+
}, )* ])
7880
}
7981

8082
fn from_index(index: usize) -> Option<Self> {

macros/src/command/mod.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ mod prefix;
22
mod slash;
33

44
use crate::util::{
5-
iter_tuple_2_to_hash_map, wrap_option, wrap_option_and_map, wrap_option_to_string,
5+
iter_tuple_2_to_vec_map, wrap_option, wrap_option_and_map, wrap_option_to_string,
66
};
77
use proc_macro::TokenStream;
88
use syn::spanned::Spanned as _;
@@ -343,9 +343,9 @@ fn generate_command(mut inv: Invocation) -> Result<proc_macro2::TokenStream, dar
343343
None => quote::quote! { Box::new(()) },
344344
};
345345

346-
let name_localizations = iter_tuple_2_to_hash_map(inv.args.name_localized.into_iter());
346+
let name_localizations = iter_tuple_2_to_vec_map(inv.args.name_localized.into_iter());
347347
let description_localizations =
348-
iter_tuple_2_to_hash_map(inv.args.description_localized.into_iter());
348+
iter_tuple_2_to_vec_map(inv.args.description_localized.into_iter());
349349

350350
let function_ident =
351351
std::mem::replace(&mut inv.function.sig.ident, syn::parse_quote! { inner });
@@ -359,6 +359,8 @@ fn generate_command(mut inv: Invocation) -> Result<proc_macro2::TokenStream, dar
359359
<#ctx_type_with_static as poise::_GetGenerics>::U,
360360
<#ctx_type_with_static as poise::_GetGenerics>::E,
361361
> {
362+
use ::std::borrow::Cow;
363+
362364
#function
363365

364366
::poise::Command {
@@ -368,11 +370,11 @@ fn generate_command(mut inv: Invocation) -> Result<proc_macro2::TokenStream, dar
368370

369371
subcommands: vec![ #( #subcommands() ),* ],
370372
subcommand_required: #subcommand_required,
371-
name: #command_name.to_string(),
373+
name: Cow::Borrowed(#command_name),
372374
name_localizations: #name_localizations,
373-
qualified_name: String::from(#command_name), // properly filled in later by Framework
374-
identifying_name: String::from(#identifying_name),
375-
source_code_name: String::from(#function_name),
375+
qualified_name: Cow::Borrowed(#command_name), // properly filled in later by Framework
376+
identifying_name: Cow::Borrowed(#identifying_name),
377+
source_code_name: Cow::Borrowed(#function_name),
376378
category: #category,
377379
description: #description,
378380
description_localizations: #description_localizations,
@@ -396,7 +398,7 @@ fn generate_command(mut inv: Invocation) -> Result<proc_macro2::TokenStream, dar
396398
parameters: vec![ #( #parameters ),* ],
397399
custom_data: #custom_data,
398400

399-
aliases: vec![ #( #aliases.to_string(), )* ],
401+
aliases: Cow::Borrowed(&[ #( Cow::Borrowed(#aliases), )* ]),
400402
invoke_on_edit: #invoke_on_edit,
401403
track_deletion: #track_deletion,
402404
broadcast_typing: #broadcast_typing,

macros/src/command/slash.rs

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use super::Invocation;
22
use crate::util::{
3-
extract_type_parameter, iter_tuple_2_to_hash_map, tuple_2_iter_deref, wrap_option_to_string,
4-
List,
3+
extract_type_parameter, iter_tuple_2_to_vec_map, tuple_2_iter_deref, wrap_option_to_string,
54
};
65
use quote::format_ident;
76
use syn::spanned::Spanned as _;
@@ -42,9 +41,9 @@ pub fn generate_parameters(inv: &Invocation) -> Result<Vec<proc_macro2::TokenStr
4241

4342
let param_name = &param.name;
4443
let name_localizations =
45-
iter_tuple_2_to_hash_map(tuple_2_iter_deref(&param.args.name_localized));
44+
iter_tuple_2_to_vec_map(tuple_2_iter_deref(&param.args.name_localized));
4645
let desc_localizations =
47-
iter_tuple_2_to_hash_map(tuple_2_iter_deref(&param.args.description_localized));
46+
iter_tuple_2_to_vec_map(tuple_2_iter_deref(&param.args.description_localized));
4847

4948
let autocomplete_callback = match &param.args.autocomplete {
5049
Some(autocomplete_fn) => {
@@ -91,37 +90,34 @@ pub fn generate_parameters(inv: &Invocation) -> Result<Vec<proc_macro2::TokenStr
9190
};
9291
// TODO: theoretically a problem that we don't store choices for non slash commands
9392
// TODO: move this to poise::CommandParameter::choices (is there a reason not to?)
94-
let choices = match inv.args.slash_command {
95-
true => {
96-
if let Some(List(choices)) = &param.args.choices {
97-
let choices = choices
98-
.iter()
99-
.map(lit_to_string)
100-
.collect::<Result<Vec<_>, _>>()?;
101-
102-
quote::quote! { vec![#( ::poise::CommandParameterChoice {
103-
name: String::from(#choices),
104-
localizations: Default::default(),
105-
__non_exhaustive: (),
106-
} ),*] }
107-
} else {
108-
quote::quote! { poise::slash_argument_choices!(#type_) }
109-
}
93+
let choices = if inv.args.slash_command {
94+
if let Some(choices) = &param.args.choices {
95+
let choices_iter = choices.0.iter();
96+
let choices: Vec<_> = choices_iter.map(lit_to_string).collect::<Result<_, _>>()?;
97+
98+
quote::quote! { Cow::Borrowed(&[#( ::poise::CommandParameterChoice {
99+
name: Cow::Borrowed(#choices),
100+
localizations: Cow::Borrowed(&[]),
101+
__non_exhaustive: (),
102+
} ),*]) }
103+
} else {
104+
quote::quote! { poise::slash_argument_choices!(#type_) }
110105
}
111-
false => quote::quote! { vec![] },
106+
} else {
107+
quote::quote! { Cow::Borrowed(&[]) }
112108
};
113109

114110
let channel_types = match &param.args.channel_types {
115111
Some(crate::util::List(channel_types)) => quote::quote! { Some(
116-
vec![ #( poise::serenity_prelude::ChannelType::#channel_types ),* ]
112+
Cow::Borrowed(&[ #( poise::serenity_prelude::ChannelType::#channel_types ),* ])
117113
) },
118114
None => quote::quote! { None },
119115
};
120116

121117
parameter_structs.push((
122118
quote::quote! {
123119
::poise::CommandParameter {
124-
name: #param_name.to_string(),
120+
name: ::std::borrow::Cow::Borrowed(#param_name),
125121
name_localizations: #name_localizations,
126122
description: #description,
127123
description_localizations: #desc_localizations,

macros/src/util.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ pub fn wrap_option_and_map<T: quote::ToTokens>(
3838
}
3939

4040
pub fn wrap_option_to_string<T: quote::ToTokens>(literal: Option<T>) -> syn::Expr {
41-
let to_string_path = quote::quote!(::std::string::ToString::to_string);
42-
wrap_option_and_map(literal, to_string_path)
41+
let cowstr_path = quote::quote!(Cow::Borrowed);
42+
wrap_option_and_map(literal, cowstr_path)
4343
}
4444

4545
/// Syn Fold to make all lifetimes 'static. Used to access trait items of a type without having its
@@ -99,13 +99,13 @@ where
9999
.map(|Tuple2(t, v)| Tuple2(t.deref(), v.deref()))
100100
}
101101

102-
pub fn iter_tuple_2_to_hash_map<I, T>(v: I) -> proc_macro2::TokenStream
102+
pub fn iter_tuple_2_to_vec_map<I, T>(v: I) -> proc_macro2::TokenStream
103103
where
104104
I: ExactSizeIterator<Item = Tuple2<T>>,
105105
T: quote::ToTokens,
106106
{
107107
if v.len() == 0 {
108-
return quote::quote!(std::collections::HashMap::new());
108+
return quote::quote!(Cow::Borrowed(&[]));
109109
}
110110

111111
let (keys, values) = v
@@ -114,8 +114,8 @@ where
114114
.unzip::<_, _, Vec<_>, Vec<_>>();
115115

116116
quote::quote! {
117-
std::collections::HashMap::from([
118-
#( (#keys.to_string(), #values.to_string()) ),*
117+
Cow::Borrowed(&[
118+
#( (Cow::Borrowed(#keys), Cow::Borrowed(#values)) ),*
119119
])
120120
}
121121
}

src/builtins/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ pub async fn autocomplete_command<'a, U, E>(
235235
.take(25);
236236

237237
let choices = filtered_commands
238-
.map(|cmd| serenity::AutocompleteChoice::from(&cmd.name))
238+
.map(|cmd| serenity::AutocompleteChoice::from(cmd.name.as_ref()))
239239
.collect();
240240

241241
serenity::CreateAutocompleteResponse::new().set_choices(choices)

0 commit comments

Comments
 (0)