diff --git a/api/v1/cu29-runtime.txt b/api/v1/cu29-runtime.txt index 981b0aa28b4..5ce4628117a 100644 --- a/api/v1/cu29-runtime.txt +++ b/api/v1/cu29-runtime.txt @@ -1790,10 +1790,16 @@ pub fn cu29_runtime::resource::ResourceManager::borrow_shared_arc Self pub fn cu29_runtime::resource::ResourceManager::take(&mut self, key: cu29_runtime::resource::ResourceKey) -> cu29_traits::CuResult> pub struct cu29_runtime::resource::ThreadPoolBundle +impl cu29_runtime::resource::NamedResourceBundleDecl for cu29_runtime::resource::ThreadPoolBundle +pub const cu29_runtime::resource::ThreadPoolBundle::NAMES: &'static [&'static str] impl cu29_runtime::resource::ResourceBundle for cu29_runtime::resource::ThreadPoolBundle pub fn cu29_runtime::resource::ThreadPoolBundle::build(bundle: cu29_runtime::resource::BundleContext, config: core::option::Option<&cu29_runtime::config::ComponentConfig>, manager: &mut cu29_runtime::resource::ResourceManager) -> cu29_traits::CuResult<()> impl cu29_runtime::resource::ResourceBundleDecl for cu29_runtime::resource::ThreadPoolBundle pub type cu29_runtime::resource::ThreadPoolBundle::Id = cu29_runtime::resource::ThreadPoolId +pub trait cu29_runtime::resource::NamedResourceBundleDecl: cu29_runtime::resource::ResourceBundleDecl +pub const cu29_runtime::resource::NamedResourceBundleDecl::NAMES: &'static [&'static str] +impl cu29_runtime::resource::NamedResourceBundleDecl for cu29_runtime::resource::ThreadPoolBundle +pub const cu29_runtime::resource::ThreadPoolBundle::NAMES: &'static [&'static str] pub trait cu29_runtime::resource::ResourceBindings<'r>: core::marker::Sized pub type cu29_runtime::resource::ResourceBindings::Binding: core::marker::Copy + core::cmp::Eq + 'static pub fn cu29_runtime::resource::ResourceBindings::from_bindings(manager: &'r mut cu29_runtime::resource::ResourceManager, mapping: core::option::Option<&cu29_runtime::resource::ResourceBindingMap>) -> cu29_traits::CuResult diff --git a/components/res/cu_linux_resources/src/lib.rs b/components/res/cu_linux_resources/src/lib.rs index adb1072e52f..842e9a488d1 100644 --- a/components/res/cu_linux_resources/src/lib.rs +++ b/components/res/cu_linux_resources/src/lib.rs @@ -313,21 +313,21 @@ pub struct LinuxResources; bundle_resources!( LinuxResources: - Serial0, - Serial1, - Serial2, - Serial3, - Serial4, - Serial5, - I2c0, - I2c1, - I2c2, - Gpio0, - Gpio1, - Gpio2, - Gpio3, - Gpio4, - Gpio5 + Serial0 = "serial0", + Serial1 = "serial1", + Serial2 = "serial2", + Serial3 = "serial3", + Serial4 = "serial4", + Serial5 = "serial5", + I2c0 = "i2c0", + I2c1 = "i2c1", + I2c2 = "i2c2", + Gpio0 = "gpio0", + Gpio1 = "gpio1", + Gpio2 = "gpio2", + Gpio3 = "gpio3", + Gpio4 = "gpio4", + Gpio5 = "gpio5" ); const LINUX_RESOURCE_SLOT_NAMES: &[&str] = &[ @@ -862,6 +862,17 @@ fn get_u64(config: Option<&ComponentConfig>, key: &str) -> CuResult> #[cfg(test)] mod tests { use super::*; + use cu29::resource::{NamedResourceBundleDecl, resource_index_by_name}; + + #[test] + fn named_resource_bundle_decl_matches_public_linux_slot_names() { + let declared = ::NAMES; + assert_eq!(declared, LINUX_RESOURCE_SLOT_NAMES); + + for (idx, name) in declared.iter().enumerate() { + assert_eq!(resource_index_by_name::(name), idx); + } + } #[test] fn parse_serial_parity_value_accepts_expected_inputs() { diff --git a/components/res/cu_micoairh743/src/lib.rs b/components/res/cu_micoairh743/src/lib.rs index 4c22a2a43b4..1c6b50b60f0 100644 --- a/components/res/cu_micoairh743/src/lib.rs +++ b/components/res/cu_micoairh743/src/lib.rs @@ -373,19 +373,19 @@ pub struct MicoAirH743; bundle_resources!( MicoAirH743: - Uart6, - Uart3, - Uart2, - Uart4, - GreenLed, - Logger, - Bmi088Spi, - Bmi088AccCs, - Bmi088GyrCs, - Bmi088Delay, - I2C2Dps310, - I2C2Ist8310, - BatteryAdc + Uart6 = "uart6", + Uart3 = "uart3", + Uart2 = "uart2", + Uart4 = "uart4", + GreenLed = "green_led", + Logger = "logger", + Bmi088Spi = "bmi088_spi", + Bmi088AccCs = "bmi088_acc_cs", + Bmi088GyrCs = "bmi088_gyr_cs", + Bmi088Delay = "bmi088_delay", + I2C2Dps310 = "i2c2_dps310", + I2C2Ist8310 = "i2c2_ist8310", + BatteryAdc = "battery_adc" ); impl ResourceBundle for MicoAirH743 { diff --git a/core/cu29_derive/src/bundle_resources.rs b/core/cu29_derive/src/bundle_resources.rs index 1e9501ee140..7de0a95469c 100644 --- a/core/cu29_derive/src/bundle_resources.rs +++ b/core/cu29_derive/src/bundle_resources.rs @@ -24,10 +24,12 @@ pub fn bundle_resources(input: TokenStream) -> TokenStream { .unwrap_or_else(|| Ident::new("Bundle", proc_macro2::Span::call_site())); let enum_ident = format_ident!("{}Id", bundle_ident); let count = ids.len(); - let variants = ids.iter(); - let names = ids.iter().map(|ident| { - let name = ident.to_string().to_case(Case::Snake); - syn::LitStr::new(&name, ident.span()) + let variants = ids.iter().map(|entry| &entry.ident); + let names = ids.iter().map(|entry| { + entry.name.clone().unwrap_or_else(|| { + let name = entry.ident.to_string().to_case(Case::Snake); + syn::LitStr::new(&name, entry.ident.span()) + }) }); let names_ident_str = format!("{}_NAMES", enum_ident.to_string().to_case(Case::UpperSnake)); let names_ident = format_ident!("{}", names_ident_str); @@ -51,6 +53,10 @@ pub fn bundle_resources(input: TokenStream) -> TokenStream { type Id = #enum_ident; } + impl ::cu29::resource::NamedResourceBundleDecl for #bundle { + const NAMES: &'static [&'static str] = #names_ident; + } + #[allow(dead_code)] pub const #names_ident: &[&str] = &[#(#names),*]; }; @@ -60,17 +66,36 @@ pub fn bundle_resources(input: TokenStream) -> TokenStream { struct BundleResourcesMacro { bundle: Path, - ids: Vec, + ids: Vec, +} + +struct BundleResourceEntry { + ident: Ident, + name: Option, } impl Parse for BundleResourcesMacro { fn parse(input: ParseStream<'_>) -> syn::Result { let bundle: Path = input.parse()?; input.parse::()?; - let ids: Punctuated = input.parse_terminated(Ident::parse, Token![,])?; + let ids: Punctuated = + input.parse_terminated(BundleResourceEntry::parse, Token![,])?; Ok(BundleResourcesMacro { bundle, ids: ids.into_iter().collect(), }) } } + +impl Parse for BundleResourceEntry { + fn parse(input: ParseStream<'_>) -> syn::Result { + let ident: Ident = input.parse()?; + let name = if input.peek(Token![=]) { + input.parse::()?; + Some(input.parse()?) + } else { + None + }; + Ok(Self { ident, name }) + } +} diff --git a/core/cu29_derive/src/lib.rs b/core/cu29_derive/src/lib.rs index 9e1788a7615..7da6b22a689 100644 --- a/core/cu29_derive/src/lib.rs +++ b/core/cu29_derive/src/lib.rs @@ -7111,16 +7111,17 @@ fn build_task_resource_mappings( <<#binding_task_type as #binding_trait>::Resources<'_> as ResourceBindings>::Binding }; let entry_tokens = entries.iter().map(|spec| { - let binding_ident = - Ident::new(&config_id_to_enum(spec.binding_name.as_str()), Span::call_site()); - let resource_ident = - Ident::new(&config_id_to_enum(spec.resource_name.as_str()), Span::call_site()); + let binding_ident = Ident::new( + &config_id_to_enum(spec.binding_name.as_str()), + Span::call_site(), + ); + let resource_name = LitStr::new(spec.resource_name.as_str(), Span::call_site()); let bundle_index = spec.bundle_index; let provider_path = &spec.provider_path; quote! { (#binding_type::#binding_ident, cu29::resource::ResourceKey::new( cu29::resource::BundleIndex::new(#bundle_index), - <#provider_path as cu29::resource::ResourceBundleDecl>::Id::#resource_ident as usize, + cu29::resource::resource_index_by_name::<#provider_path>(#resource_name), )) } }); @@ -7172,16 +7173,17 @@ fn build_bridge_resource_mappings( let entries_ident = format_ident!("BRIDGE{}_RES_ENTRIES", idx); let map_ident = format_ident!("BRIDGE{}_RES_MAPPING", idx); let entry_tokens = entries.iter().map(|spec| { - let binding_ident = - Ident::new(&config_id_to_enum(spec.binding_name.as_str()), Span::call_site()); - let resource_ident = - Ident::new(&config_id_to_enum(spec.resource_name.as_str()), Span::call_site()); + let binding_ident = Ident::new( + &config_id_to_enum(spec.binding_name.as_str()), + Span::call_site(), + ); + let resource_name = LitStr::new(spec.resource_name.as_str(), Span::call_site()); let bundle_index = spec.bundle_index; let provider_path = &spec.provider_path; quote! { (#binding_type::#binding_ident, cu29::resource::ResourceKey::new( cu29::resource::BundleIndex::new(#bundle_index), - <#provider_path as cu29::resource::ResourceBundleDecl>::Id::#resource_ident as usize, + cu29::resource::resource_index_by_name::<#provider_path>(#resource_name), )) } }); diff --git a/core/cu29_derive/tests/compile_pass/copper_runtime/resource_bundle_slot_name_case.rs b/core/cu29_derive/tests/compile_pass/copper_runtime/resource_bundle_slot_name_case.rs new file mode 100644 index 00000000000..c169d41be45 --- /dev/null +++ b/core/cu29_derive/tests/compile_pass/copper_runtime/resource_bundle_slot_name_case.rs @@ -0,0 +1,85 @@ +use cu29::prelude::*; +use cu29::resource::{BundleContext, ResourceBundle, ResourceManager}; +use cu29::{bundle_resources, resources}; +use cu29_derive::copper_runtime; + +use std::string::String; + +pub struct TestBundle; + +bundle_resources!( + TestBundle: + Serial3 = "serial3", + Gpio0 = "gpio0", + I2c1 = "i2c1" +); + +impl ResourceBundle for TestBundle { + fn build( + bundle: BundleContext, + _config: Option<&ComponentConfig>, + manager: &mut ResourceManager, + ) -> CuResult<()> { + manager.add_owned(bundle.key(TestBundleId::Serial3), String::from("serial"))?; + manager.add_owned(bundle.key(TestBundleId::Gpio0), String::from("pin"))?; + manager.add_owned(bundle.key(TestBundleId::I2c1), String::from("bus"))?; + Ok(()) + } +} + +#[derive(Reflect)] +struct NoopSource; + +impl Freezable for NoopSource {} + +impl CuSrcTask for NoopSource { + type Resources<'r> = (); + type Output<'m> = output_msg!(u32); + + fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult { + Ok(Self) + } + + fn process(&mut self, _ctx: &CuContext, output: &mut Self::Output<'_>) -> CuResult<()> { + output.set_payload(1); + Ok(()) + } +} + +mod sink_resources { + use super::*; + + resources!({ + serial => Owned, + pin => Owned, + bus => Owned, + }); +} + +type SinkResources = sink_resources::Resources; + +#[derive(Reflect)] +struct UsesNamedResourcesSink; + +impl Freezable for UsesNamedResourcesSink {} + +impl CuSinkTask for UsesNamedResourcesSink { + type Resources<'r> = SinkResources; + type Input<'m> = input_msg!(u32); + + fn new(_config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult { + let _serial = resources.serial.0; + let _pin = resources.pin.0; + let _bus = resources.bus.0; + Ok(Self) + } + + fn process(&mut self, _ctx: &CuContext, _input: &Self::Input<'_>) -> CuResult<()> { + Ok(()) + } +} + +#[copper_runtime(config = "config/resource_bundle_slot_name_case_valid.ron")] +struct App {} + +fn main() {} diff --git a/core/cu29_derive/tests/config/resource_bundle_slot_name_case_valid.ron b/core/cu29_derive/tests/config/resource_bundle_slot_name_case_valid.ron new file mode 100644 index 00000000000..cca7c16e70e --- /dev/null +++ b/core/cu29_derive/tests/config/resource_bundle_slot_name_case_valid.ron @@ -0,0 +1,27 @@ +( + resources: [ + ( + id: "linux", + provider: "TestBundle", + ), + ], + tasks: [ + (id: "src", type: "NoopSource"), + ( + id: "sink", + type: "UsesNamedResourcesSink", + resources: { + "serial": "linux.serial3", + "pin": "linux.gpio0", + "bus": "linux.i2c1", + }, + ), + ], + cnx: [ + ( + src: "src", + dst: "sink", + msg: "u32", + ), + ], +) \ No newline at end of file diff --git a/core/cu29_runtime/src/resource.rs b/core/cu29_runtime/src/resource.rs index d6e5cce5932..9af5d38f24c 100644 --- a/core/cu29_runtime/src/resource.rs +++ b/core/cu29_runtime/src/resource.rs @@ -158,6 +158,49 @@ pub trait ResourceBundleDecl { type Id: ResourceId; } +/// Optional name metadata for resource bundles. +/// +/// Bundles created via `bundle_resources!` implement this automatically. The +/// derive macro uses these canonical slot names to resolve `bundle.resource` +/// bindings without guessing enum variant casing from config strings. +pub trait NamedResourceBundleDecl: ResourceBundleDecl { + const NAMES: &'static [&'static str]; +} + +const fn str_eq(left: &str, right: &str) -> bool { + let left = left.as_bytes(); + let right = right.as_bytes(); + if left.len() != right.len() { + return false; + } + + let mut idx = 0; + while idx < left.len() { + if left[idx] != right[idx] { + return false; + } + idx += 1; + } + + true +} + +/// Resolve a bundle slot name to its resource index. +/// +/// This is a `const fn` so generated resource binding tables can stay static. +#[doc(hidden)] +pub const fn resource_index_by_name(name: &str) -> usize { + let mut idx = 0; + while idx < B::NAMES.len() { + if str_eq(B::NAMES[idx], name) { + return idx; + } + idx += 1; + } + + panic!("resource slot name not declared by bundle"); +} + /// Static mapping between user-defined binding ids and resource keys. #[derive(Clone, Copy)] pub struct ResourceBindingMap { @@ -408,6 +451,11 @@ impl ResourceBundleDecl for ThreadPoolBundle { type Id = ThreadPoolId; } +#[cfg(feature = "std")] +impl NamedResourceBundleDecl for ThreadPoolBundle { + const NAMES: &'static [&'static str] = &["bg_threads"]; +} + #[cfg(feature = "std")] impl ResourceBundle for ThreadPoolBundle { fn build( @@ -436,3 +484,41 @@ impl ResourceBundle for ThreadPoolBundle { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Copy, Clone, Eq, PartialEq)] + #[repr(usize)] + enum DummyBundleId { + Uart0, + I2c1, + } + + impl ResourceId for DummyBundleId { + const COUNT: usize = 2; + + fn index(self) -> usize { + self as usize + } + } + + struct DummyBundle; + + impl ResourceBundleDecl for DummyBundle { + type Id = DummyBundleId; + } + + impl NamedResourceBundleDecl for DummyBundle { + const NAMES: &'static [&'static str] = &["uart0", "i2c1"]; + } + + #[test] + fn resource_index_by_name_matches_declared_slot_name() { + assert_eq!(DummyBundleId::Uart0.index(), 0); + assert_eq!(DummyBundleId::I2c1.index(), 1); + assert_eq!(resource_index_by_name::("uart0"), 0); + assert_eq!(resource_index_by_name::("i2c1"), 1); + } +}