From c278b2578cb44527093bfe4c0cfc9101899de134 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Mon, 27 Jul 2026 12:09:32 +0530 Subject: [PATCH 01/25] Rust::com Method Interface APIs and Macro update * Created Method related interface traits * Updated Interface macro * created method related macro --- .../mw/com/rust/score_com_concept/concept.rs | 17 +- .../score_com_concept/interface_macros.rs | 732 ++++++++++++++++-- score/mw/com/rust/score_com_concept/lib.rs | 4 + score/mw/com/rust/score_com_concept/method.rs | 313 ++++++++ .../method_arities_macros.rs | 158 ++++ score/mw/com/rust/score_com_concept/reloc.rs | 7 - 6 files changed, 1175 insertions(+), 56 deletions(-) create mode 100644 score/mw/com/rust/score_com_concept/method.rs create mode 100644 score/mw/com/rust/score_com_concept/method_arities_macros.rs diff --git a/score/mw/com/rust/score_com_concept/concept.rs b/score/mw/com/rust/score_com_concept/concept.rs index 8e5f7601b..b55ba260a 100644 --- a/score/mw/com/rust/score_com_concept/concept.rs +++ b/score/mw/com/rust/score_com_concept/concept.rs @@ -49,13 +49,14 @@ //! - Tuples use crate::error::*; +use crate::method::*; use crate::Reloc; -pub use score_com_macros::CommData; use containers::fixed_capacity::FixedCapacityQueue; use core::fmt::Debug; use core::future::Future; use core::ops::{Deref, DerefMut}; use futures::stream::Stream; +pub use score_com_macros::CommData; use std::path::Path; /// Result type alias with `std::result::Result` using `score_com::Error` as error type @@ -100,6 +101,14 @@ pub trait Runtime { /// `Publisher` types for Publishes event data to subscribers type Publisher: Publisher; + type MethodInArgAllocator: MethodInArgAllocator; + + /// `MethodCaller` types for calling methods on the proxy/consumer side + type MethodCaller: MethodCaller; + + /// `MethodHandler` types for handling method calls on the skeleton/producer side + type MethodHandler: MethodHandler; + /// `ProviderInfo` types for Configuration data for service producers instances type ProviderInfo: ProviderInfo + Send + Clone; @@ -210,6 +219,12 @@ pub trait CommData: Reloc { const ID: &'static str; } +// Arity-0 unit tuple — special-cased here; arities 1+ are generated by +// `impl_all_arities!` in `method_arities.rs`. +impl CommData for () { + const ID: &'static str = "()"; +} + /// Technology independent description of a service instance "location" /// /// The string shall describe where to find a certain instance of a service. Each level shall look diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_macros.rs index 46cae701f..757004136 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -10,6 +10,21 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +// Type-state marker for handler not registered (compile-time tracking). +#[allow(dead_code)] +pub struct Uninit; + +/// Type-state marker for initialized field state (compile-time tracking). +#[allow(dead_code)] +pub struct Init; + +/// Type-state marker for handler not registered (compile-time tracking). +#[allow(dead_code)] +pub struct HandlerNotSet; + +/// Type-state marker for handler registered (compile-time tracking). +#[allow(dead_code)] +pub struct HandlerSet; /// Main interface macro that generates Consumer, Producer, and OfferedProducer types /// along with all necessary trait implementations. @@ -17,23 +32,27 @@ /// Automatically generates unique type names from the identifier of macro invocation. /// For an interface with identifier `{id}`, it generates: /// - `{id}Interface` - Struct representing the interface with INTERFACE_ID constant -/// - `{id}Consumer` - Consumer implementation with event subscribers +/// - `{id}Consumer` - Consumer implementation with event subscribers, field subscribers, +/// and method callers /// - `{id}Producer` - Producer implementation -/// - `{id}OfferedProducer` - Offered producer implementation with event publishers +/// - `{id}OfferedProducer` - Offered producer implementation with event publishers, +/// field publishers, and method handlers /// - Implements the `Interface`, `Consumer`, `Producer`, and `OfferedProducer` traits /// for the respective types. /// - `Interface_ID` is generated by default as the module path + interface name, /// but can be overridden by providing a custom UID as a second parameter to the macro. /// -/// Parameters: -/// - Keywords: `interface` followed by the interface identifier and a block of event definitions. -/// - `$id`: Simple identifier used for type name generation (e.g., Vehicle, Engine) -/// - `$event_name`: Event field name -/// - `$event_type`: Event data type +/// # Member types +/// - `name: Event` - event subscriber / publisher pair +/// - `name: Field` - field subscriber / publisher pair (with set-handler callback support) +/// - `name(Args) -> Return` - method caller / handler pair (fn-like syntax) /// -/// Example usage: +/// # Parameters +/// - Keywords: `interface` followed by the interface identifier and a block of member definitions. +/// - `$id`: Simple identifier used for type name generation (e.g., Vehicle, Engine) +/// - Members can be any mix of `Event`, `Field`, and `name(Args) -> Return` /// -/// With default UID generation (module path + interface name): +/// # Example: Event-only with auto-generated ID /// ```ignore /// mod abc { /// use score_com::interface; @@ -47,14 +66,9 @@ /// ``` /// The generated code will include: /// - `VehicleInterface` struct with `INTERFACE_ID = "abc::Vehicle"` -/// - `VehicleConsumer` struct that implements `Consumer` trait for subscribing to -/// "left_tire" and "exhaust" events. -/// - `VehicleProducer` struct that implements `Producer` trait for producing -/// "left_tire" and "exhaust" events. -/// - `VehicleOfferedProducer` struct that implements `OfferedProducer` trait for offering -/// "left_tire" and "exhaust" events. +/// - `VehicleConsumer`, `VehicleProducer`, `VehicleOfferedProducer` /// -/// With custom UID: +/// # Example: Mixed interface (Event + Field + Method) with custom ID /// ```ignore /// mod abc { /// use score_com::interface; @@ -62,7 +76,8 @@ /// interface Vehicle { /// Id = "AbcInterface", /// left_tire: Event, -/// exhaust: Event, +/// left_tire_field: Field, +/// left_tire_method(Tire) -> Tire, /// } /// ); /// } @@ -70,22 +85,33 @@ /// Here Id is explicitly set to "AbcInterface" instead of the default "abc::Vehicle". /// The generated code will include: /// - `VehicleInterface` struct with `INTERFACE_ID = "AbcInterface"` -/// - `VehicleConsumer` struct that implements `Consumer` trait for subscribing -/// to "left_tire" and "exhaust" events. -/// - `VehicleProducer` struct that implements `Producer` trait for producing -/// "left_tire" and "exhaust" events. -/// - `VehicleOfferedProducer` struct that implements `OfferedProducer` trait for offering -/// "left_tire" and "exhaust" events. +/// - `VehicleConsumer` with `left_tire: Subscriber`, `left_tire_field: FieldSubscriber`, +/// `left_tire_method: MethodCaller<(Tire,), Tire>` and a convenience `left_tire_method(arg0: Tire)` method. +/// - `VehicleProducer` (derives `TypeStateValidator`) with `left_tire_field: FieldPublisher`, +/// `left_tire_method: MethodHandler<(Tire,), Tire>`. Requires `.init()` chain before `.offer()`. +/// - `VehicleOfferedProducer` with event publisher `left_tire`, plus moved field publisher and +/// method handler. +/// Main interface macro that supports Event-only interfaces (backward compatible) and +/// mixed interfaces containing any combination of `Event`, `Field`, and +/// `method_name(Args) -> Return` members in the same definition block. +/// +/// # Backward-compatible arms (unchanged) +/// Event-only interfaces continue to work without any changes. +/// +/// # Mixed / unified arms +/// When the body contains anything other than a homogeneous list of `Event` members +/// (i.e., any `Field` or fn-like method member), the recursive-macro arms parse the body +/// and delegate to `interface_consumer_mixed!` / `interface_producer_mixed!`. #[macro_export] macro_rules! interface { - // Default unique ID based on the module path and interface name + // Backward-compatible: Event-only, auto-generated ID (interface $id:ident { $($event_name:ident : Event<$event_type:ty>),+ $(,)? }) => { $crate::interface_common!($id); $crate::interface_consumer!($id, $($event_name, Event<$event_type>),+); $crate::interface_producer!($id, $($event_name, Event<$event_type>),+); }; - // Custom unique Id provided by the user + // Backward-compatible: Event-only, custom ID (interface $id:ident { Id = $uid:expr, $($event_name:ident : Event<$event_type:ty>),+ $(,)? @@ -95,33 +121,167 @@ macro_rules! interface { $crate::interface_producer!($id, $($event_name, Event<$event_type>),+); }; - // This is for backward compatibility for existing users with comma (,) + // Backward-compatible: Event-only with comma separator (legacy syntax) (interface $id:ident, { Id = $uid:expr, $($event_name:ident : Event<$event_type:ty>),+ $(,)? }) => { $crate::interface! { interface $id { - Id = $uid, - $($event_name : Event<$event_type>),+ - }} + Id = $uid, + $($event_name : Event<$event_type>),+ + } + } }; - (interface $id:ident { $($event_name:ident : Method<$event_type:ty>),+$(,)? }) => { - compile_error!( - "Method definitions are not supported in this macro version. \ - Please use Event syntax for defining events." + // Mixed / unified: custom ID - MUST come before auto-ID catch-all + (interface $id:ident { + Id = $uid:expr, + $($members:tt)* + }) => { + $crate::interface_common!($id, $uid); + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[] + @fi[] + @me[] + $($members)* ); }; - (interface $id:ident { $($event_name:ident : Field<$event_type:ty>),+$(,)? }) => { - compile_error!( - "Field definitions are not supported in this macro version. \ - Please use Event syntax for defining events." + // Mixed / unified: auto-generated ID - catch-all, must come last. + (interface $id:ident { $($members:tt)* }) => { + $crate::interface_common!($id); + $crate::_interface_collect_members!( + @id[$id, concat!(module_path!(), "::", stringify!($id))] + @ev[] + @fi[] + @me[] + $($members)* ); }; } +/// Internal recursive-macro helper for `interface!`. +/// +/// Accumulates members into three typed lists, then calls the mixed generator macros. +/// +/// Accumulator format: +/// ```text +/// @id[$id, $uid] +/// @ev[$($ev_name : $ev_type ,)*] +/// @fi[$($fi_name : $fi_type ,)*] +/// @me[$($me_name ($me_args) -> $me_ret ,)*] +/// ``` +#[doc(hidden)] +#[macro_export] +macro_rules! _interface_collect_members { + // Base case: nothing left - emit the mixed consumer and producer + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $(,)? + ) => { + $crate::interface_consumer_mixed!( + $id, + events[$($ev_name : $ev_type ,)*], + fields[$($fi_name : $fi_type ,)*], + methods[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + ); + $crate::interface_producer_mixed!( + $id, + events[$($ev_name : $ev_type ,)*], + fields[$($fi_name : $fi_type ,)*], + methods[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + ); + }; + + // Event member: `name : Event ,?` + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $name:ident : Event<$t:ty> + $(, $($rest:tt)*)? + ) => { + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)* $name : $t ,] + @fi[$($fi_name : $fi_type ,)*] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + $($($rest)*)? + ); + }; + + // Field member: `name : Field ,?` + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $name:ident : Field<$t:ty> + $(, $($rest:tt)*)? + ) => { + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)*] + @fi[$($fi_name : $fi_type ,)* $name : $t ,] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + $($($rest)*)? + ); + }; + + // Method member (fn-like syntax): `name(Arg0, Arg1, ...) -> Ret ,?` + // Positional types - no tuple wrapper needed at the user level. + // Internally stored as a bracketed list: name [Arg0, Arg1, ...] -> Ret + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $name:ident ( $($arg_ty:ty),* ) -> $ret:ty + $(, $($rest:tt)*)? + ) => { + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)*] + @fi[$($fi_name : $fi_type ,)*] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)* $name [$($arg_ty),*] -> $ret ,] + $($($rest)*)? + ); + }; + + // Catch-all: unrecognized member - emit a clear compile-time error. + ( + @id[$_id:ident, $_uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $($unknown:tt)+ + ) => { + compile_error!(concat!( + "interface!: unrecognized member syntax: `", + stringify!($($unknown)+), + "`.\n", + "Supported member types:\n", + " name: Event - event subscriber / publisher pair\n", + " name: Field - field subscriber / publisher pair\n", + " name(Arg0, Arg1, ...) -> Ret - method caller / handler pair\n", + "Example:\n", + " interface!(interface MyIface {\n", + " my_event: Event,\n", + " my_field: Field,\n", + " my_method(MyData) -> MyData,\n", + " my_void_method(MyData) -> (),\n", + " my_no_arg_method() -> MyData,\n", + " });" + )); + }; +} + /// Macro to create a unique interface struct and implement the Interface trait for it. /// /// Generates the INTERFACE_ID constant and associated Consumer/Producer types. @@ -157,6 +317,7 @@ macro_rules! interface_common { /// Macro to implement the Consumer trait for a given interface ID and its events. /// /// Generates the Consumer struct with subscribers for each event. +// TODO: This can be removed once verification is done that the new interface_producer_mixed! macro works for event-only interfaces. #[macro_export] macro_rules! interface_consumer { ($id:ident, $($event_name:ident, Event<$event_type:ty>),+$(,)?) => { @@ -186,9 +347,11 @@ macro_rules! interface_consumer { }; } +/// This is Event specific. /// Macro to implement the Producer and OfferedProducer traits for /// a given interface ID and its events. /// Generates Producer and OfferedProducer structs with publishers for each event. +// TODO: This can be removed once verification is done that the new interface_producer_mixed! macro works for event-only interfaces. #[macro_export] macro_rules! interface_producer { ($id:ident, $($event_name:ident, Event<$event_type:ty>),+$(,)?) => { @@ -252,6 +415,353 @@ macro_rules! interface_producer { }; } +/// Generates the `{id}Consumer` struct and its `Consumer` trait implementation for +/// interfaces that may contain any combination of events, fields, and methods. +/// +/// # Generated struct fields +/// - `pub $ev_name: R::Subscriber<$ev_type>` - one per event +/// - `pub $fi_name: R::FieldSubscriber<$fi_type>` - one per field +/// - `pub $me_name: R::MethodCaller<($me_arg_ty,...), $me_ret>` - one per method +/// +/// # method wrappers +/// For each method member a positional-argument `pub fn $me_name(&self, arg0: A0, ...)` wrapper +/// is generated (via `_gen_method_wrapper!`). The wrapper packs the positional args into a tuple +/// and dispatches through `MethodCallInput`, so both copy and zero-copy paths use the same call site. +/// The wrapper returns `impl Future> + '_`. +/// copy: `consumer.method(val).await` - `val: T` - copy path +/// zero-copy: `consumer.method(ptr).await` - `ptr: MethodInArgPtr` - zero-copy path +#[doc(hidden)] +#[macro_export] +macro_rules! interface_consumer_mixed { + ( + $id:ident, + events[$($ev_name:ident : $ev_type:ty ,)*], + fields[$($fi_name:ident : $fi_type:ty ,)*], + methods[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + ) => { + score_com::paste::paste! { + pub struct [<$id Consumer>] { + $( + pub $ev_name: R::Subscriber<$ev_type>, + )* + $( + pub $fi_name: R::FieldSubscriber<$fi_type>, + )* + $( + pub $me_name: R::MethodCaller<($($me_arg_ty,)*), $me_ret>, + )* + } + + impl score_com::Consumer for [<$id Consumer>] { + fn new(instance_info: R::ConsumerInfo) -> Self { + [<$id Consumer>] { + $( + $ev_name: R::Subscriber::new( + stringify!($ev_name), + instance_info.clone() + ).expect(&format!( + "Failed to create subscriber for {}", + stringify!($ev_name) + )), + )* + $( + $fi_name: R::FieldSubscriber::new( + stringify!($fi_name), + instance_info.clone() + ).expect(&format!( + "Failed to create field subscriber for {}", + stringify!($fi_name) + )), + )* + $( + $me_name: + as score_com::MethodCaller<($($me_arg_ty,)*), $me_ret, R>>::new( + stringify!($me_name), + instance_info.clone() + ).expect(&format!( + "Failed to create method caller for {}", + stringify!($me_name) + )), + )* + } + } + } + + // Positional-argument convenience wrappers - one per method member. + // The wrapper packs args into a tuple and dispatches via MethodCallInput, + // so copy and zero-copy paths share the same call site. + // copy: consumer.method_name(val).await + // zero-copy: consumer.method_name(ptr).await + impl [<$id Consumer>] { + $( + $crate::_gen_method_wrapper!($me_name ($($me_arg_ty),*) -> $me_ret); + )* + } + } + }; +} + +/// Generates `{id}Producer`, `{id}OfferedProducer`, and all trait implementations for +/// interfaces that may contain any combination of events, fields, and methods. +/// +/// # Design +/// - **Event publishers** (`R::Publisher`) are created *lazily during `_offer_internal()`* +/// so they are only present on the `OfferedProducer`. +/// - **Field publishers** (`R::FieldPublisher`) are created eagerly in `Producer::new()` and +/// moved into `OfferedProducer` when the service is offered. +/// - **Method handlers** (`R::MethodHandler`) likewise created eagerly and moved. +/// +/// When the interface has at least one field or method member, the `Producer` struct derives +/// `TypeStateValidator` which generates the `.init()` entry point and the `update_*` / +/// `register_set_handler_*` / `register_*_handler` chain required before `offer()`. +/// +/// When the interface has *only* events (no fields, no methods), a plain `offer()` is generated +/// directly (matching the existing event-only pattern). +#[doc(hidden)] +#[macro_export] +macro_rules! interface_producer_mixed { + // Event-only specialisation (no fields, no methods): + // plain offer() without type-state validation - identical to interface_producer! + ( + $id:ident, + events[$($ev_name:ident : $ev_type:ty ,)+], + fields[], + methods[] + ) => { + $crate::interface_producer!($id, $($ev_name, Event<$ev_type>),+); + }; + + // General case: at least one field or method (or both), possibly with events too. + ( + $id:ident, + events[$($ev_name:ident : $ev_type:ty ,)*], + fields[$($fi_name:ident : $fi_type:ty ,)*], + methods[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + ) => { + score_com::paste::paste! { + // Producer struct - derives TypeStateValidator for compile-time offer() gating. + // Fields: FieldPublisher per field + MethodHandler per method. + // Event publishers are NOT stored here; they are created during _offer_internal(). + #[derive($crate::score_com_concept_macros::TypeStateValidator)] + pub struct [<$id Producer>] { + $( + $fi_name: R::FieldPublisher<$fi_type>, + )* + $( + $me_name: R::MethodHandler<($($me_arg_ty,)*), $me_ret>, + )* + pub instance_info: R::ProviderInfo, + } + + // OfferedProducer struct - contains event publishers (created on offer), + // plus the moved field publishers and method handlers from Producer. + pub struct [<$id OfferedProducer>] { + $( + pub $ev_name: R::Publisher<$ev_type>, + )* + $( + pub $fi_name: R::FieldPublisher<$fi_type>, + )* + $( + $me_name: R::MethodHandler<($($me_arg_ty,)*), $me_ret>, + )* + instance_info: R::ProviderInfo, + } + + // Internal implementation - called by the TypeStateValidator's offer() after all + // states have been validated at compile time. + impl [<$id Producer>] { + #[doc(hidden)] + pub fn _offer_internal( + self, + ) -> score_com::Result<[<$id OfferedProducer>]> { + let offered = [<$id OfferedProducer>] { + $( + $ev_name: R::Publisher::new( + stringify!($ev_name), + self.instance_info.clone() + ).expect(&format!( + "Failed to create publisher for {}", + stringify!($ev_name) + )), + )* + $( + $fi_name: self.$fi_name, + )* + $( + $me_name: self.$me_name, + )* + instance_info: self.instance_info.clone(), + }; + self.instance_info.offer_service()?; + Ok(offered) + } + } + + // We can not remove the offer method from the Producer trait, but we can override it to panic with a clear message. + // Also adding compiler warning or error for this is not possible, we will rely on documentation and panic. + // if user call this directly, then it will panic and it is against the intended usage of the APIs. + // TODO: Need to think about this more, when we have more complex interface with mixed types. + // Also update the documentation for this, so user should not call offer() directly from Producer struct. + impl score_com::Producer for [<$id Producer>] { + type Interface = [<$id Interface>]; + type OfferedProducer = [<$id OfferedProducer>]; + + fn offer(self) -> score_com::Result { + panic!( + "ERROR: Cannot call {producer}.offer() directly.\n\ + All fields must be initialized and all handlers must be registered first.\n\ + Correct usage: producer.init()\ + .update_(&val)?\ + .register_set_handler_(|v| {{ ... }})\ + .register__handler(|args| {{ ... }})\ + .offer()?", + producer = stringify!([<$id Producer>]) + ) + } + + fn new(instance_info: R::ProviderInfo) -> score_com::Result { + Ok([<$id Producer>] { + $( + $fi_name: R::FieldPublisher::new( + stringify!($fi_name), + instance_info.clone() + )?, + )* + $( + $me_name: + as score_com::MethodHandler<($($me_arg_ty,)*), $me_ret, R>>::new( + stringify!($me_name), + instance_info.clone() + )?, + )* + instance_info, + }) + } + } + + // OfferedProducer trait impl - unoffer() stops the service and returns the Producer. + impl score_com::OfferedProducer + for [<$id OfferedProducer>] + { + type Interface = [<$id Interface>]; + type Producer = [<$id Producer>]; + + fn unoffer(self) -> score_com::Result { + self.instance_info.stop_offer_service()?; + Ok([<$id Producer>] { + $( + $fi_name: self.$fi_name, + )* + $( + $me_name: self.$me_name, + )* + instance_info: self.instance_info, + }) + } + } + } + }; +} + +/// Entry-point wrapper generator. +/// Every generated wrapper returns `impl Future> + '_`. +/// +/// # Generated call sites +/// ```text +/// consumer.method(val).await - copy path - val: ArgType +/// consumer.method(ptr).await - zero-copy - ptr: MethodInArgPtr +/// ``` +#[doc(hidden)] +#[macro_export] +macro_rules! _gen_method_wrapper { + // 0 args - invoke_with_copy directly; no zero-copy path (nothing to allocate). + // This is for kind of `get` methods that take no arguments and return a value. + ($me_name:ident () -> $me_ret:ty) => { + pub fn $me_name<'a>(&'a self) -> impl core::future::Future> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.$me_name, ()) + } + }; + // 1–N args - delegate to the self-counting recursive macro. + ($me_name:ident ($($t:ty),+) -> $me_ret:ty) => { + $crate::_gen_method_wrapper_collect!( + $me_name -> $me_ret ; + @counter[] + @acc[] + @types[$($t),+] + ); + }; +} + +/// Recursive macro for `_gen_method_wrapper!`. +/// +/// Self-counting: instead of zipping the method's positional type list against a +/// pre-defined pool of `(arg_name, generic_name)` identifiers, this recursive macro synthesizes +/// a fresh, unique `(argN : _AN : TypeN)` triplet at each recursion step directly from a +/// growing counter of `n` marker tokens (via `paste!`), then calls +/// `_gen_method_wrapper_body!` once the type list is exhausted. +/// +/// This mirrors the self-contained recursion used by `impl_all_arities!` in +/// `method_arities.rs`: there is no separate pool to keep in sync, and no fixed +/// argument-count limit - any arity supported by `method_arities.rs` works automatically. +#[doc(hidden)] +#[macro_export] +macro_rules! _gen_method_wrapper_collect { + // Base: all types consumed - emit the function via the body macro. + ( + $me_name:ident -> $me_ret:ty ; + @counter[$($n:tt)*] + @acc[$($acc:tt),*] + @types[] + ) => { + $crate::_gen_method_wrapper_body!($me_name -> $me_ret ; [$($acc),*]); + }; + + // Step: consume one type, grow the counter by one `n`, and synthesize a fresh + // (param, generic) identifier pair from the counter via `paste!`. + ( + $me_name:ident -> $me_ret:ty ; + @counter[$($n:tt)*] + @acc[$($acc:tt),*] + @types[$t:ty $(, $rest_t:ty)*] + ) => { + score_com::paste::paste! { + $crate::_gen_method_wrapper_collect!( + $me_name -> $me_ret ; + @counter[$($n)* n] + @acc[$($acc,)* ([] : [<_A $($n)*>] : $t)] + @types[$($rest_t),*] + ); + } + }; +} + +/// Generates the wrapper function from an accumulated list of `(argN : _AN : TypeN)`. +/// +/// This generates a wrapper function template. +/// All arities use this one arm - the function body is written once, not duplicated per arity. +/// Called by `_gen_method_wrapper_collect!` after it has built the full triplet list. +/// +/// The generated function returns `impl Future> + 'a` so callers +/// can `.await` the method call, e.g. `consumer.method_name(arg0).await?`. +#[doc(hidden)] +#[macro_export] +macro_rules! _gen_method_wrapper_body { + ($me_name:ident -> $me_ret:ty ; [$(($p:ident : $g:ident : $c:ty)),+]) => { + pub fn $me_name<'a, $($g),+>( + &'a self, + $($p: $g),+ + ) -> impl core::future::Future> + 'a + where + ($($g,)+): score_com::MethodCallInput<($($c,)+), $me_ret, R>, + R::MethodCaller<($($c,)+), $me_ret>: + score_com::MethodCaller<($($c,)+), $me_ret, R>, + { + score_com::MethodCallInput::invoke(($($p,)+), &self.$me_name) + } + }; +} + mod tests { /// ``` /// mod my_module { @@ -353,7 +863,9 @@ mod tests { #[cfg(doctest)] fn interface_macro_with_custom_id_with_comma_for_backend_compatibility() {} - /// ```compile_fail + /// Mixed interface (Event + Field + Method) with a custom ID. + /// + /// ```ignore /// mod my_module { /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; /// @@ -373,17 +885,26 @@ mod tests { /// /// interface!( /// interface Vehicle { - /// Id = "CustomVehicleInterface", - /// left_tire: Method, - /// exhaust: Method, + /// Id = "AbcInterface", + /// left_tire: Event, + /// left_tire_field: Field, + /// left_tire_method(Tire) -> Tire, /// } /// ); /// } /// ``` - /// This will fail to compile because the macro does not support Method definitions and will - /// produce a compile-time error indicating that Method definitions are not supported. + /// Generates `VehicleInterface`, `VehicleConsumer`, `VehicleProducer`, + /// and `VehicleOfferedProducer` where: + /// - `VehicleConsumer` has `left_tire: Subscriber`, + /// `left_tire_field: FieldSubscriber`, + /// `left_tire_method: MethodCaller<(Tire,), Tire>`, + /// and a convenience `left_tire_method(arg0: Tire)` method. + /// - `VehicleProducer` derives `TypeStateValidator` and requires the `.init()` chain: + /// `producer.init().update_left_tire_field(&val)?.register_set_handler_left_tire_field(f).register_left_tire_method_handler(h).offer()?` + /// - `VehicleOfferedProducer` has `left_tire: Publisher` (created lazily on offer), + /// `left_tire_field: FieldPublisher`, plus the active method handler. #[cfg(doctest)] - fn interface_macro_with_Method() {} + fn interface_macro_mixed() {} /// ```compile_fail /// mod my_module { @@ -405,14 +926,36 @@ mod tests { /// /// interface!( /// interface Vehicle { - /// left_tire: Field, - /// exhaust: Field, + /// Id = "CustomVehicleInterface", + /// left_tire: Method, + /// exhaust: Method, /// } /// ); /// } /// ``` - /// This will fail to compile because the macro does not support Field definitions and will - /// produce a compile-time error indicating that Field definitions are not supported. + /// This will fail to compile because `Method` (old syntax without a return type) is not + /// supported. Use fn-like syntax: `method_name(Args) -> Ret`. + #[cfg(doctest)] + fn interface_macro_with_old_method_syntax() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface_common!(Vehicle, "CustomVehicleInterface", { + /// left_tire: Event, + /// }); + /// } + /// ``` + /// This will fail to compile because `interface_common!` does not accept member definitions. + /// Use `interface!` for a complete interface definition. #[cfg(doctest)] fn interface_macro_with_Field() {} @@ -1054,4 +1597,97 @@ mod validation_tests { } test_module::validate(); } + + #[test] + fn test_mixed_interface_types_generated() { + mod test_module { + use score_com::{ + CommData, Interface, LolaRuntimeImpl as LolaRuntime, ProviderInfo, Publisher, + Reloc, Subscriber, + }; + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Tire { + pub pressure: f32, + } + impl CommData for Tire { + const ID: &'static str = "Tire"; + } + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Exhaust {} + impl CommData for Exhaust { + const ID: &'static str = "Exhaust"; + } + + crate::interface!( + interface VehicleMixed { + Id = "VehicleMixedInterface", + left_tire: Event, + exhaust_field: Field, + update_pressure(Tire) -> Tire, + } + ); + + pub fn validate() { + // Verify custom interface ID. + let interface_id = ::INTERFACE_ID; + assert_eq!(interface_id, "VehicleMixedInterface"); + + // Verify all four types are generated with correct names. + let _ = core::marker::PhantomData::; + let _ = core::marker::PhantomData::>; + let _ = core::marker::PhantomData::>; + let _ = core::marker::PhantomData::>; + + // Verify Consumer struct size (confirms fields were generated). + assert!( + std::mem::size_of::>() > 0, + "VehicleMixedConsumer should have member fields" + ); + } + } + test_module::validate(); + } + + #[test] + fn test_mixed_interface_event_only_via_recursive_macro() { + // Verifies that a mixed-arm interface with only events still generates + // the same types as the backward-compatible Event-only arm. + mod test_module { + use score_com::{CommData, Interface, LolaRuntimeImpl as LolaRuntime, Reloc}; + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Signal { + pub value: u32, + } + impl CommData for Signal { + const ID: &'static str = "Signal"; + } + + // This goes through the recursive-macro path (mixed arm) but with only events. + crate::interface!( + interface Radar { + target: Event, + velocity: Event, + } + ); + + pub fn validate() { + let interface_id = ::INTERFACE_ID; + assert_eq!( + interface_id, + concat!(module_path!(), "::", "Radar"), + "Auto-generated ID should include module path" + ); + let _ = core::marker::PhantomData::>; + let _ = core::marker::PhantomData::>; + let _ = core::marker::PhantomData::>; + } + } + test_module::validate(); + } } diff --git a/score/mw/com/rust/score_com_concept/lib.rs b/score/mw/com/rust/score_com_concept/lib.rs index 920c9b7bb..8dbad6b21 100644 --- a/score/mw/com/rust/score_com_concept/lib.rs +++ b/score/mw/com/rust/score_com_concept/lib.rs @@ -23,9 +23,13 @@ mod concept; mod error; mod interface_macros; +mod method; +mod method_arities_macros; mod reloc; pub use concept::*; pub use error::*; +pub use interface_macros::{HandlerNotSet, HandlerSet}; +pub use method::*; #[doc(hidden)] pub use paste; pub use reloc::Reloc; diff --git a/score/mw/com/rust/score_com_concept/method.rs b/score/mw/com/rust/score_com_concept/method.rs new file mode 100644 index 000000000..fc36cc8a8 --- /dev/null +++ b/score/mw/com/rust/score_com_concept/method.rs @@ -0,0 +1,313 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/// For method as rust side does not have any varadic function argument support, +/// so we are having tuple of arguments, so we can have any number of arguments (currently up to 2) without any extra boilerplate. +/// we have implemeted blanket implementation of MethodArgs, MethodArgsAllocate and +/// MethodCallInput traits for all supported arities (0–2 arguments) are provided in this crate. +/// This blanket implementation help in design to have any number of arguments +/// (currently up to 2) without runtime specific implementation for each arity. +/// This crate provides the necessary traits and types to support method calls in a communication API, +/// including handling of method arguments, allocation of uninitialized argument, +/// and invocation of methods with both copy and zero-copy semantics. +/// Which enable the interface macro to generate exactly a single consumer method per interface method - +/// instead of two separate copy and zero-copy methods. +/// We want to follow the same semantics for method like c++ provide for method call, +/// and because of that we have added few supporting traits which help to create similar semantics for method call in rust side. +/// In the event and field we have `SampleMut` with that allocated memory can call send, +/// but in method call we can not use that approach as we do not have any common API to call send/update, +/// Method takes the argument whether it is by value or by zero-copy in same method function/method, +/// because of that we have added `invoke_with_copy` and `invoke_zero_copy` methods in MethodCaller trait. +/// Which is not user facing APIs but used by interface macro and supporting traits. +/// +/// trait details: +/// MethodHandler: Producer side registration of method handlers, +/// this needs to be implemented by runtime for producer side method handler registration. +/// MethodCaller: Consumer side caller of methods, this needs to be implemented by runtime for consumer side method calls. +/// This trait provides methods for invoking methods with both copy and zero-copy semantics, +/// also handles allocation of uninitialized method arguments for zero-copy calls, +/// this is user facing API for consumer side method calls. +/// This trait is used by the interface macro to generate consumer methods for each interface method and +/// invoke the runtime specific method caller implementation. +/// MethodInArgMaybeUninit: This is the uninitialized type for a single method argument, +/// it is used in the zero-copy method call path. +/// MethodInArgAllocator: This is for runtime-specific method argument allocation, +/// it is used in the zero-copy method call path. +/// Which provide the allocate API for specific argument type and +/// return the uninitialized method argument type for that argument type. +/// +/// Now below traits are marker / marker-like (because it is implemented for all supported arities) traits and +/// which no need to implement by runtime because blanket implementation is added in this crate. +/// +/// MethodArgs: Marker trait for method argument tuples, +/// this is used to carry the matching tuple of MethodInArgPtr used in the zero-copy call path. +/// MethodArgsAllocate: Maps an Args tuple type to the matching uninitialized method argument tuple for a specific runtime allocator A, +/// this is used to produce the uninitialized method arguments for zero-copy method call path. +/// MethodCallInput: Unified input for a method call accepted by the interface macro-generated consumer methods, +/// this is used to dispatch the method call to the appropriate runtime specific method - +/// caller implementation based on the type of the input arguments. +/// MethodHandlerCall: Callable handler function for a method with Args inputs and Return output, +/// this is used to register the handler function for a method on the producer side, +/// which can be a plain closure or any FnMut with the matching signature, +/// and it will automatically satisfy this trait, +/// so that the interface macro can generate the necessary code to register the handler function for each interface method on the producer side. +/// +// TODO: Add a blocking `.wait()` convenience for method-call futures, for sync callers who don't +// want to bring their own async executor (similar in spirit to `futures::executor::block_on`). +use crate::concept::*; +use core::future::Future; + +// This is a pointer type for a pre-allocated method argument. It is used in the zero-copy method call path. +// TODO: Remove this once memory layout implementation is added in rust side, same like samplePtr. +// Also need to check about lifetime of this pointer and add all the trait or type which is required. +pub struct MethodInArgPtr { + pub _phantom: core::marker::PhantomData, +} + +/// Producer side registration of method handlers. +/// This is the interface that a producer implements to register handlers for its methods. +pub trait MethodHandler { + /// Create a new method handler for the given method name and instance info. + /// + /// # Arguments + /// * `method_name` - The name of the method to handle. + /// * `instance_info` - The provider instance info for the method handler. + /// + /// Returns a `Result` containing the new method handler or an error if the creation failed. + fn new(method_name: &str, instance_info: R::ProviderInfo) -> Result + where + Self: Sized; + + /// Register a handler function for the method. + /// which is automatically satisfied by any function or closure with the appropriate signature. + /// + /// # Arguments + /// * `handler` - The handler function to register for the method, which has to bound the `MethodHandlerCall` trait blanket implementation. + fn register_handler(&self, handler: F) + where + F: MethodHandlerCall; +} + +/// Consumer side caller of methods. +/// This is the interface that a consumer implements to call methods on a producer. +/// In this trait we have two methods for method call, one is `invoke_with_copy` and another is `invoke_zero_copy`, +/// Which are not intended to be used by user directly, but used by interface macro to generate consumer methods for each interface method. +/// This used by runtime to implement the specific implementation for method call. +/// Both call methods return a future so callers can `.await` the result. +pub trait MethodCaller { + /// Create a new method caller for the given method name and instance info. + /// + /// # Arguments + /// * `method_name` - The name of the method to call. + /// * `instance_info` - The consumer instance info for the method call. + /// + /// Returns a `Result` containing the new method caller or an error if the creation failed. + fn new(method_name: &str, instance_info: R::ConsumerInfo) -> Result + where + Self: Sized; + + /// Invoke the method with copied arguments. This is the copy path for method calls. + /// + /// # Arguments + /// * `args` - The method arguments to pass to the method call. + /// + /// Returns a future that resolves to a `Result` containing the method return value if any + /// otherwise unit or an error if the call failed. + fn invoke_with_copy<'a>(&'a self, args: Args) -> impl Future> + 'a; + + /// Allocate uninitialized method arguments for a zero-copy method call. + /// + /// Returns a `Result` containing the uninitialized method arguments tuple or an error if the allocation failed. + /// + /// Note: This method returns the tuple of uninitialized method arguments for the given `Args` type, + /// which can then be written individually and passed to the method. + /// Here `Args` is a tuple of method argument types for the given method, + /// and `UninitTuple` is the corresponding tuple of uninitialized method argument types for the given runtime's method argument allocator. + /// e.g., for a method with signature `fn my_method(arg1: T1, arg2: T2) -> Return`, the `Args` type would be `(T1, T2)`, + /// and the `UninitTuple` type would be `(A::MethodInArgMaybeUninit, A::MethodInArgMaybeUninit)` where `A` is the runtime's method argument allocator. + fn allocate( + &self, + ) -> Result<>::UninitTuple> + where + Args: MethodArgsAllocate; + + /// Invoke the method with zero-copy arguments. This is the zero-copy path for method calls. + /// + /// # Arguments + /// * `ptrs` - The pre-allocated method argument pointers to pass to the method call in a tuple. + /// + /// Returns a future that resolves to a `Result` containing the method return value if any + /// otherwise unit or an error if the call failed. + fn invoke_zero_copy<'a>( + &'a self, + ptrs: ::PtrTuple, + ) -> impl Future> + 'a; +} + +/// This is the uninitialized type for a single method argument. It is used in the zero-copy method call path. +/// Allocate method returns a tuple of these uninitialized method types, which can then be written to and passed to the method call. +/// +/// # Note: +/// `MethodCaller::allocate()` returns a tuple of `MethodInArgMaybeUninit` values. The current +/// API does not enforce at compile time that all method arguments in the tuple are written before +/// method is called. A user can call `assume_init()` on an unwritten method argument, which +/// is undefined behaviour once real shared memory backs these method arguments. +/// +/// TODO: We can consider adding a typesatate or builder pattern to enforce this, if required. +pub trait MethodInArgMaybeUninit { + /// Write a value into this pre-allocated method argument and return the initialized pointer. + fn write(self, val: T) -> MethodInArgPtr; + + /// Assume the method argument is already initialized and return the pointer. + /// + /// # Safety + /// The caller must ensure the memory has been properly initialized before calling this. + unsafe fn assume_init(self) -> MethodInArgPtr; +} + +/// This is for runtime-specific method argument allocation. It is used in the zero-copy method call path. +/// This trait provides the allocate API for specific argument type and return the uninitialized method argument type for that argument type. +pub trait MethodInArgAllocator { + /// The concrete uninitialized method argument type this allocator produces for argument type `T`. + type MethodInArgMaybeUninit: MethodInArgMaybeUninit; + + /// Produce a new uninitialized method argument for argument type `T`. + /// + /// Returns a `MethodInArgMaybeUninit` which can then be written to and passed to the method call. + fn allocate(&self) -> Self::MethodInArgMaybeUninit; +} + +// Below traits are supporting traits and which no need to implement by runtime. +// These are implemented in this crate and used by interface macro to generate consumer and producer code. +// And supporting methods with any number of arguments based on the `impl_all_arities!` macro in this crate. + +/// Marker trait for method argument tuples. +/// +/// Carries `PtrTuple` - the matching tuple of `MethodInArgPtr` used in the zero-copy call path. +/// For example, `(Tire, Tire)::PtrTuple = (MethodInArgPtr, MethodInArgPtr)`. +/// +/// Runtimes do not implement this trait. +/// Blanket impls for all supported arities (0–2 arguments) are provided in this crate. +pub trait MethodArgs: CommData { + type PtrTuple; +} + +// Arity-0 unit tuple - special-cased here; arities 1+ are generated by +// `impl_all_arities!` in `method_arities.rs`. +impl MethodArgs for () { + type PtrTuple = (); +} + +/// Maps an `Args` tuple type to the matching uninitialized method argument tuple for a specific runtime allocator `A`. +/// +/// Given an allocator `A` (e.g. `LolaMethodInArgAllocator`) and an args tuple (e.g. `(Tire, Tire)`), +/// `UninitTuple` becomes `(A::MethodInArgMaybeUninit, A::MethodInArgMaybeUninit)`. +/// `MethodCaller::allocate()` calls `alloc_uninit()` internally to produce these uninitialized method arguments. +/// +/// Runtimes do not implement this trait. +/// Blanket impls for all supported arities are provided in this crate. +pub trait MethodArgsAllocate: MethodArgs { + type UninitTuple; + /// Allocate uninitialized method arguments for all parameters using the provided allocator instance. + /// + /// Returns a tuple of uninitialized method arguments corresponding to the `Args` tuple type. + fn alloc_uninit(allocator: &A) -> Self::UninitTuple; +} + +// Arity-0 unit tuple - special-cased here; arities 1+ are generated by +// `impl_all_arities!` in `method_arities.rs`. +impl MethodArgsAllocate for () { + type UninitTuple = (); + fn alloc_uninit(_allocator: &A) {} +} + +/// Unified input for a method call accepted by the interface macro-generated consumer methods. +/// +/// Allows the `interface!` macro to generate exactly a single consumer method per interface method +/// instead of two separate copy and zero-copy methods. Implemented for: +/// - `Args` itself - dispatches to `invoke_with_copy` (copy path) +/// - `MethodInArgPtr,...` - dispatches to `invoke_zero_copy` (zero-copy path) +/// +/// The compiler selects the right impl purely from the type passed at the call site, no runtime branching. +/// +/// Runtimes do not implement this trait. +/// All impls are provided in this crate. Adding a new runtime only requires implementing +/// `MethodCaller`, the dispatch through `MethodCallInput` works automatically. +pub trait MethodCallInput { + /// Invoke the method with the given input, dispatching to the appropriate runtime-specific method caller. + /// + /// # Arguments + /// * `caller` - The runtime-specific method caller to use for the invocation. + /// + /// Returns a future that resolves to a `Result` containing the method return value if any + /// otherwise unit or an error if the call failed. + fn invoke<'a>( + self, + caller: &'a R::MethodCaller, + ) -> impl Future> + 'a + where + R::MethodCaller: MethodCaller + 'a; +} + +/// Copy path: blanket impl - arity-agnostic, no per-arity duplication needed. +/// Copy path: pass `Args` directly. +impl MethodCallInput for Args +where + Args: MethodArgs + CommData, + Return: CommData, + R: Runtime + ?Sized, + R::MethodCaller: MethodCaller, +{ + fn invoke<'a>( + self, + caller: &'a R::MethodCaller, + ) -> impl Future> + 'a + where + R::MethodCaller: MethodCaller + 'a, + { + as MethodCaller>::invoke_with_copy( + caller, self, + ) + } +} + +// Zero-copy path: all arities 1+ are generated by `impl_all_arities!` in `method_arities.rs`. + +/// Callable handler function for a method with `Args` inputs and `Return` output. +/// +/// Application code on the producer side passes a plain closure to `MethodHandler::register_handler`; +/// any `Fn` with the matching signature automatically satisfies this trait. +/// +/// `Fn` (immutable receiver) is required rather than `FnMut` because the runtime may dispatch +/// concurrent calls from a thread pool. +/// TODO: We can think about adding `FnMut` support in the future, +/// but it would require a synchronization mechanism in the runtime to ensure that concurrent calls do not violate the `FnMut` contract. +/// So this can be decided at the implementation time of the runtime, whether it wants to support `FnMut` or not. +/// +/// Runtimes do not implement this trait. +/// Blanket impls for all supported arities are provided in this crate so that closures just work without any extra boilerplate. +pub trait MethodHandlerCall: Send + Sync + 'static { + /// Call the handler function with the given arguments and return the result. + fn call(&self, args: Args) -> Return; +} + +// Arity-0 unit tuple - special-cased here; arities 1+ are generated by +// `impl_all_arities!` in `method_arities.rs`. +impl MethodHandlerCall<(), Return> for F +where + F: Fn() -> Return + Send + Sync + 'static, +{ + fn call(&self, _args: ()) -> Return { + (self)() + } +} diff --git a/score/mw/com/rust/score_com_concept/method_arities_macros.rs b/score/mw/com/rust/score_com_concept/method_arities_macros.rs new file mode 100644 index 000000000..f0078025b --- /dev/null +++ b/score/mw/com/rust/score_com_concept/method_arities_macros.rs @@ -0,0 +1,158 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +//! Arity-parameterised blanket impls for method argument tuples. +//! +//! # Single configuration point +//! +//! To raise (or lower) the maximum number of arguments a method may have, edit the +//! `impl_all_arities!` invocation at the bottom of this file. Add one +//! more `(TypeIdent, arg_ident)` pair per additional argument. Everything else - +//! `Reloc`, `CommData`, `MethodArgs`, `MethodArgsAllocate`, `MethodCallInput` (zero-copy +//! path), and `MethodHandlerCall` - is generated using macros. +//! +//! `_gen_method_wrapper!` in `interface_macros.rs` self-generates its argument +//! identifiers via a counting recursive macro, so it has no separate limit to keep in +//! sync - raising the arity here is the only change needed. +//! (I don't think this many arguments will support by clippy linting, +//! so we may need to reduce the limit to 8 or 10 in the future based on project clippy linting rules.) +//! +//! # Arity 0 special case +//! +//! Arity 0 (`()`) is handled separately in `com_api_method.rs` because the zero-tuple +//! has no positional variables to destructure. This macro covers arities **1 and above**. +//! +//! # How the recursive macro works +//! +//! The macro maintains two accumulated token lists in parallel: +//! - **Type vars** `[T1, T2, …]` - used as generic parameters in trait impls. +//! - **Arg names** `[a0, a1, …]` - used for positional destructuring inside +//! `MethodHandlerCall::call` and `MethodCallInput::invoke`. +//! +//! At each step the next `(TypeIdent, arg_ident)` pair is peeled from the input, the +//! two accumulated lists grow by one, all six impls for the new arity are emitted, and +//! the recursion continues with the extended lists. + +use crate::{ + CommData, MethodArgs, MethodArgsAllocate, MethodCallInput, MethodCaller, MethodHandlerCall, + MethodInArgAllocator, MethodInArgPtr, Reloc, Result, Runtime, +}; +use core::future::Future; + +/// Internal recursive macro. Do not invoke directly - use `impl_all_arities!` below. +#[doc(hidden)] +macro_rules! impl_all_arities { + ( $( ($T:ident, $a:ident) ),+ $(,)? ) => { + impl_all_arities!(@step [] [] [$( ($T, $a) ),+]); + }; + + (@step [$($T:ident),*] [$($a:ident),*] []) => {}; + + ( + @step [$($T:ident),*] [$($a:ident),*] + [($nextT:ident, $nextA:ident) $(, ($restT:ident, $restA:ident))*] + ) => { + + unsafe impl<$($T: Reloc,)* $nextT: Reloc> Reloc for ($($T,)* $nextT,) {} + + impl<$($T: CommData,)* $nextT: CommData> CommData for ($($T,)* $nextT,) { + // Placeholder ID - the tuple structure itself serves as the identifier. + const ID: &'static str = stringify!(($($T,)* $nextT,)); + } + + impl<$($T: CommData,)* $nextT: CommData> MethodArgs for ($($T,)* $nextT,) { + type PtrTuple = ($( MethodInArgPtr<$T>, )* MethodInArgPtr<$nextT>,); + } + + impl<$($T: CommData,)* $nextT: CommData, _Alloc: MethodInArgAllocator> + MethodArgsAllocate<_Alloc> for ($($T,)* $nextT,) + { + type UninitTuple = ( + $( _Alloc::MethodInArgMaybeUninit<$T>, )* + _Alloc::MethodInArgMaybeUninit<$nextT>, + ); + + fn alloc_uninit(allocator: &_Alloc) -> Self::UninitTuple { + ($( allocator.allocate::<$T>(), )* allocator.allocate::<$nextT>(),) + } + } + + // Accepts a tuple of `MethodInArgPtr` values and dispatches to + // `invoke_zero_copy`. The copy path is already covered by the blanket impl + // in `com_api_method.rs` and does not need to be repeated here. + impl<$($T: CommData,)* $nextT: CommData, Return: CommData, R: Runtime + ?Sized> + MethodCallInput<($($T,)* $nextT,), Return, R> + for ($( MethodInArgPtr<$T>, )* MethodInArgPtr<$nextT>,) + where + R::MethodCaller<($($T,)* $nextT,), Return>: + MethodCaller<($($T,)* $nextT,), Return, R>, + { + fn invoke<'a>( + self, + caller: &'a R::MethodCaller<($($T,)* $nextT,), Return>, + ) -> impl Future> + 'a + where + R::MethodCaller<($($T,)* $nextT,), Return>: + MethodCaller<($($T,)* $nextT,), Return, R> + 'a, + { + // Destructure with positional arg names, then reconstruct the ptr tuple. + #[allow(non_snake_case)] + let ($($a,)* $nextA,) = self; + as + MethodCaller<($($T,)* $nextT,), Return, R>>::invoke_zero_copy( + caller, + ($($a,)* $nextA,), + ) + } + } + + // Maps a plain `Fn(T1, T2, …) -> Return` closure to the tuple-based call + // convention used by the runtime. + impl<_F, $($T,)* $nextT, Return> MethodHandlerCall<($($T,)* $nextT,), Return> for _F + where + _F: Fn($($T,)* $nextT,) -> Return + Send + Sync + 'static, + { + fn call(&self, args: ($($T,)* $nextT,)) -> Return { + #[allow(non_snake_case)] + let ($($a,)* $nextA,) = args; + (self)($($a,)* $nextA,) + } + } + + impl_all_arities!( + @step [$($T,)* $nextT] [$($a,)* $nextA] + [$( ($restT, $restA) ),*] + ); + }; +} + +// Single configuration point +// +// To raise the maximum method argument count: +// 1. Add one more `(TypeIdent, arg_ident)` pair below. +// 2. That's it - all six trait impls are generated automatically. +// +// `_gen_method_wrapper!` (`interface_macros.rs`) has no fixed arity limit of its own, +// so raising the limit here is the only change needed. +// +// Current limit: 8 arguments. +impl_all_arities!( + (T1, a0), + (T2, a1), + (T3, a2), + (T4, a3), + (T5, a4), + (T6, a5), + (T7, a6), + (T8, a7), +); diff --git a/score/mw/com/rust/score_com_concept/reloc.rs b/score/mw/com/rust/score_com_concept/reloc.rs index 4e751ae40..2bce7f8a3 100644 --- a/score/mw/com/rust/score_com_concept/reloc.rs +++ b/score/mw/com/rust/score_com_concept/reloc.rs @@ -54,10 +54,3 @@ unsafe impl Reloc for [T; N] {} // MaybeUninit unsafe impl Reloc for core::mem::MaybeUninit {} - -// Tuples (up to 5 elements) -unsafe impl Reloc for (T1,) {} -unsafe impl Reloc for (T1, T2) {} -unsafe impl Reloc for (T1, T2, T3) {} -unsafe impl Reloc for (T1, T2, T3, T4) {} -unsafe impl Reloc for (T1, T2, T3, T4, T5) {} From a0b03591c2705b76fad526733433ea38639ad9b2 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Mon, 27 Jul 2026 12:22:11 +0530 Subject: [PATCH 02/25] Rust::com Type State Pattern derive macro added * For generating type state pattern for method and field * Validating offer API call --- .../mw/com/rust/score_com_concept/concept.rs | 2 +- score/mw/com/rust/score_com_concept/lib.rs | 4 +- .../{method.rs => method_concept.rs} | 0 score/mw/com/rust/score_com_macros/BUILD | 5 +- score/mw/com/rust/score_com_macros/lib.rs | 46 +++ .../score_com_macros/type_state_validator.rs | 376 ++++++++++++++++++ 6 files changed, 429 insertions(+), 4 deletions(-) rename score/mw/com/rust/score_com_concept/{method.rs => method_concept.rs} (100%) create mode 100644 score/mw/com/rust/score_com_macros/type_state_validator.rs diff --git a/score/mw/com/rust/score_com_concept/concept.rs b/score/mw/com/rust/score_com_concept/concept.rs index b55ba260a..ed2e11c07 100644 --- a/score/mw/com/rust/score_com_concept/concept.rs +++ b/score/mw/com/rust/score_com_concept/concept.rs @@ -49,7 +49,7 @@ //! - Tuples use crate::error::*; -use crate::method::*; +use crate::method_concept::*; use crate::Reloc; use containers::fixed_capacity::FixedCapacityQueue; use core::fmt::Debug; diff --git a/score/mw/com/rust/score_com_concept/lib.rs b/score/mw/com/rust/score_com_concept/lib.rs index 8dbad6b21..c316f3053 100644 --- a/score/mw/com/rust/score_com_concept/lib.rs +++ b/score/mw/com/rust/score_com_concept/lib.rs @@ -23,13 +23,13 @@ mod concept; mod error; mod interface_macros; -mod method; mod method_arities_macros; +mod method_concept; mod reloc; pub use concept::*; pub use error::*; pub use interface_macros::{HandlerNotSet, HandlerSet}; -pub use method::*; +pub use method_concept::*; #[doc(hidden)] pub use paste; pub use reloc::Reloc; diff --git a/score/mw/com/rust/score_com_concept/method.rs b/score/mw/com/rust/score_com_concept/method_concept.rs similarity index 100% rename from score/mw/com/rust/score_com_concept/method.rs rename to score/mw/com/rust/score_com_concept/method_concept.rs diff --git a/score/mw/com/rust/score_com_macros/BUILD b/score/mw/com/rust/score_com_macros/BUILD index 25ec089ac..0f64f34e3 100644 --- a/score/mw/com/rust/score_com_macros/BUILD +++ b/score/mw/com/rust/score_com_macros/BUILD @@ -15,7 +15,10 @@ load("@rules_rust//rust:defs.bzl", "rust_doc_test", "rust_proc_macro") rust_proc_macro( name = "score-com-macros", - srcs = ["lib.rs"], + srcs = [ + "lib.rs", + "type_state_validator.rs", + ], crate_name = "score_com_macros", visibility = [ "//score/mw/com:__subpackages__", diff --git a/score/mw/com/rust/score_com_macros/lib.rs b/score/mw/com/rust/score_com_macros/lib.rs index e09f43402..8cfba9ab6 100644 --- a/score/mw/com/rust/score_com_macros/lib.rs +++ b/score/mw/com/rust/score_com_macros/lib.rs @@ -15,6 +15,8 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, parse_quote, Data, DeriveInput, Fields, Generics, Meta, Type}; +mod type_state_validator; + /// Derive macro for the `CommData` trait. /// /// Implements `CommData` for a struct or C-like enum, providing a stable string identity @@ -335,6 +337,50 @@ fn collect_field_types(data: &Data) -> Result, ()> { Ok(out) } +/// Unified derive macro for compile-time type-state validation of Field and Method producers. +/// +/// Detects member types by the last segment of each field's type path: +/// - `FieldPublisher` → generates `update_{name}()` and `register_set_handler_{name}()` +/// - `MethodHandler` → generates `register_{name}_handler()` +/// - `instance_info` field is always skipped. +/// +/// # Generated validator struct +/// +/// `{Name}Validator` where: +/// - `Si` = field update state (`Uninit` / `Init`) +/// - `Hi` = field set-handler state (`HandlerNotSet` / `HandlerSet`) +/// - `Mj` = method handler state (`HandlerNotSet` / `HandlerSet`) +/// +/// `offer()` is only available when ALL `Si = Init`, ALL `Hi = HandlerSet`, ALL `Mj = HandlerSet`. +/// +/// Entry point on the producer: `init()` — begins the type-state chain. +/// +/// Degenerates correctly: +/// - Field-only struct → no `Mj` params +/// - Method-only struct → no `Si`/`Hi` params +/// - Mixed struct → all param groups combined +/// +/// # Usage +/// +/// ```ignore +/// #[derive(TypeStateValidator)] +/// struct VehicleProducer { +/// left_tire: R::FieldPublisher, +/// process: R::MethodHandler<(Tire,), Tire>, +/// instance_info: R::ProviderInfo, +/// } +/// // Generated: producer.init() +/// // .update_left_tire(&v)? +/// // .register_set_handler_left_tire(|v| {}) +/// // .register_process_handler(|req| { ... }) +/// // .offer()? +/// ``` +// TODO: Document tests need to be added for this macro, including successful and failed compilation cases. +#[proc_macro_derive(TypeStateValidator)] +pub fn derive_typestate_validator(input: TokenStream) -> TokenStream { + type_state_validator::derive_typestate_validator_impl(input) +} + // Use doctest to test failed compilations and successful ones /// ``` diff --git a/score/mw/com/rust/score_com_macros/type_state_validator.rs b/score/mw/com/rust/score_com_macros/type_state_validator.rs new file mode 100644 index 000000000..f608a19a2 --- /dev/null +++ b/score/mw/com/rust/score_com_macros/type_state_validator.rs @@ -0,0 +1,376 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Fields, Type}; + +/// Unified type-state validator for producers containing `FieldPublisher` and/or +/// `MethodHandler` members. +/// +/// Detects member type by the last segment of each field's type path: +/// - `FieldPublisher` - generates `update_{name}()` (Uninit - Init) and +/// `register_set_handler_{name}()` (HandlerNotSet - HandlerSet) per member. +/// - `MethodHandler` - generates `register_{name}_handler()` +/// (HandlerNotSet - HandlerSet) per member. +/// - `instance_info` field is always skipped. +/// +/// # Generated validator struct +/// +/// `{Name}Validator` where: +/// - `Si` tracks update state of field member `i` (`Uninit` / `Init`) +/// - `Hi` tracks set-handler state of field member `i` (`HandlerNotSet` / `HandlerSet`) +/// - `Mj` tracks handler state of method member `j` (`HandlerNotSet` / `HandlerSet`) +/// +/// `offer()` is only generated for the impl where ALL `Si = Init`, ALL `Hi = HandlerSet`, +/// ALL `Mj = HandlerSet`. It calls `_offer_internal()` on the wrapped producer. +/// +/// Entry point on the producer: `init()` - returns the validator with every state +/// parameter set to its initial value (`Uninit` / `HandlerNotSet`). +pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + // Extract runtime generic parameter from the first generic param of the struct. + let (runtime_param_name, runtime_param_with_bounds) = + if let Some(param) = input.generics.params.first() { + match param { + syn::GenericParam::Type(type_param) => { + let n = &type_param.ident; + (quote! { #n }, quote! { #param }) + } + _ => (quote! { R }, quote! { R: score_com::Runtime + ?Sized }), + } + } else { + (quote! { R }, quote! { R: score_com::Runtime + ?Sized }) + }; + + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + _ => { + return syn::Error::new_spanned( + name, + "TypeStateValidator only supports structs with named fields", + ) + .to_compile_error() + .into(); + } + }, + _ => { + return syn::Error::new_spanned(name, "TypeStateValidator only supports structs") + .to_compile_error() + .into(); + } + }; + + // Classify each field by the last segment of its type path. + // Note: these string names ("FieldPublisher", "MethodHandler") must match the trait/type + // names used in the Runtime associated types. If those names change, update here too. + struct FieldMember { + ident: syn::Ident, + inner_ty: Type, // T extracted from FieldPublisher + } + struct MethodMember { + ident: syn::Ident, + args_ty: Type, // Args extracted from MethodHandler + return_ty: Type, // Return extracted from MethodHandler + } + + let mut field_members: Vec = Vec::new(); + let mut method_members: Vec = Vec::new(); + + for f in fields.iter() { + let ident = match f.ident.as_ref() { + Some(i) => i.clone(), + None => continue, + }; + // Skip the bookkeeping field — it carries no type-state. + if ident == "instance_info" { + continue; + } + + if let Type::Path(type_path) = &f.ty { + if let Some(segment) = type_path.path.segments.last() { + match segment.ident.to_string().as_str() { + "FieldPublisher" => { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { + field_members.push(FieldMember { + ident, + inner_ty: inner.clone(), + }); + } + } + } + "MethodHandler" => { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if args.args.len() >= 2 { + if let ( + Some(syn::GenericArgument::Type(args_ty)), + Some(syn::GenericArgument::Type(return_ty)), + ) = (args.args.get(0), args.args.get(1)) + { + method_members.push(MethodMember { + ident, + args_ty: args_ty.clone(), + return_ty: return_ty.clone(), + }); + } + } + } + } + _ => {} // Other fields (e.g. PhantomData) are ignored. + } + } + } + } + + if field_members.is_empty() && method_members.is_empty() { + return syn::Error::new_spanned( + name, + "TypeStateValidator: no FieldPublisher or MethodHandler fields found \ + (excluding instance_info)", + ) + .to_compile_error() + .into(); + } + + let validator_name = syn::Ident::new(&format!("{}Validator", name), name.span()); + + // State param naming: + // S{i} — update state for field member i (Uninit / Init) + // H{i} — set-handler state for field member i (HandlerNotSet / HandlerSet) + // M{j} — handler state for method member j (HandlerNotSet / HandlerSet) + // Combined order in the validator struct: [S0..Sn, H0..Hn, M0..Mm] + let field_update_params: Vec = (0..field_members.len()) + .map(|i| syn::Ident::new(&format!("S{}", i), proc_macro::Span::call_site().into())) + .collect(); + let field_handler_params: Vec = (0..field_members.len()) + .map(|i| syn::Ident::new(&format!("H{}", i), proc_macro::Span::call_site().into())) + .collect(); + let method_handler_params: Vec = (0..method_members.len()) + .map(|j| syn::Ident::new(&format!("M{}", j), proc_macro::Span::call_site().into())) + .collect(); + + // Flat list used in struct definition and impl generics: [S0..Sn, H0..Hn, M0..Mm] + let all_params: Vec<&syn::Ident> = field_update_params + .iter() + .chain(field_handler_params.iter()) + .chain(method_handler_params.iter()) + .collect(); + + // Initial states for init() entry point. + let init_states: Vec<_> = (0..field_members.len()) + .map(|_| quote! { ::score_com::Uninit }) + .chain((0..field_members.len()).map(|_| quote! { ::score_com::HandlerNotSet })) + .chain((0..method_members.len()).map(|_| quote! { ::score_com::HandlerNotSet })) + .collect(); + + // All-satisfied states required by offer(). + let done_states: Vec<_> = (0..field_members.len()) + .map(|_| quote! { ::score_com::Init }) + .chain((0..field_members.len()).map(|_| quote! { ::score_com::HandlerSet })) + .chain((0..method_members.len()).map(|_| quote! { ::score_com::HandlerSet })) + .collect(); + + // update_{name}() impls for each field member + // Transitions Si: Uninit - Init while all other state params stay generic. + let update_methods: Vec<_> = field_members + .iter() + .enumerate() + .map(|(i, member)| { + let update_fn = + syn::Ident::new(&format!("update_{}", member.ident), member.ident.span()); + let inner_ty = &member.inner_ty; + let field_ident = &member.ident; + + // After-state list: Si becomes Init, every other param stays generic. + let after: Vec<_> = all_params + .iter() + .enumerate() + .map(|(k, p)| { + if k == i { + quote! { ::score_com::Init } + } else { + quote! { #p } + } + }) + .collect(); + + quote! { + impl<#runtime_param_with_bounds, #(#all_params),*> + #validator_name<#runtime_param_name, #(#all_params),*> + { + pub fn #update_fn( + mut self, + value: &#inner_ty, + ) -> score_com::Result<#validator_name<#runtime_param_name, #(#after),*>> { + self.producer.#field_ident.update(value)?; + Ok(#validator_name { + producer: self.producer, + _phantom: core::marker::PhantomData, + }) + } + } + } + }) + .collect(); + + // register_set_handler_{name}() impls for each field member + // Hi is at index field_members.len() + i in all_params. + // Transitions Hi: HandlerNotSet - HandlerSet while all other state params stay generic. + let register_set_handler_methods: Vec<_> = field_members + .iter() + .enumerate() + .map(|(i, member)| { + let register_fn = syn::Ident::new( + &format!("register_set_handler_{}", member.ident), + member.ident.span(), + ); + let inner_ty = &member.inner_ty; + let field_ident = &member.ident; + let hi_index = field_members.len() + i; + + let after: Vec<_> = all_params + .iter() + .enumerate() + .map(|(k, p)| { + if k == hi_index { + quote! { ::score_com::HandlerSet } + } else { + quote! { #p } + } + }) + .collect(); + + quote! { + impl<#runtime_param_with_bounds, #(#all_params),*> + #validator_name<#runtime_param_name, #(#all_params),*> + where + <#runtime_param_name as score_com::Runtime>::FieldPublisher<#inner_ty>: Send, + { + pub fn #register_fn( + mut self, + handler: F, + ) -> #validator_name<#runtime_param_name, #(#after),*> + where + F: Fn(&#inner_ty) + Send + 'static, + { + self.producer.#field_ident.register_set_handler(handler); + #validator_name { + producer: self.producer, + _phantom: core::marker::PhantomData, + } + } + } + } + }) + .collect(); + + // register_{name}_handler() impls for each method member + // Mj is at index 2 * field_members.len() + j in all_params. + // Transitions Mj: HandlerNotSet - HandlerSet while all other state params stay generic. + let register_handler_methods: Vec<_> = method_members + .iter() + .enumerate() + .map(|(j, member)| { + let register_fn = syn::Ident::new( + &format!("register_{}_handler", member.ident), + member.ident.span(), + ); + let args_ty = &member.args_ty; + let return_ty = &member.return_ty; + let method_ident = &member.ident; + let mj_index = 2 * field_members.len() + j; + + let after: Vec<_> = all_params + .iter() + .enumerate() + .map(|(k, p)| { + if k == mj_index { + quote! { ::score_com::HandlerSet } + } else { + quote! { #p } + } + }) + .collect(); + + quote! { + impl<#runtime_param_with_bounds, #(#all_params),*> + #validator_name<#runtime_param_name, #(#all_params),*> + { + pub fn #register_fn( + mut self, + handler: F, + ) -> #validator_name<#runtime_param_name, #(#after),*> + where + F: score_com::MethodHandlerCall<#args_ty, #return_ty>, + { + <_ as score_com::MethodHandler<#args_ty, #return_ty, #runtime_param_name>>::register_handler( + &self.producer.#method_ident, + handler, + ); + #validator_name { + producer: self.producer, + _phantom: core::marker::PhantomData, + } + } + } + } + }) + .collect(); + + let expanded = quote! { + // Validator struct type params track state of every Field and Method member. + // Layout: + pub struct #validator_name<#runtime_param_with_bounds, #(#all_params),*> { + producer: #name<#runtime_param_name>, + _phantom: core::marker::PhantomData<(#(#all_params,)*)>, + } + + // update_{name}() - transitions Si: Uninit - Init + #(#update_methods)* + + // register_set_handler_{name}() - transitions Hi: HandlerNotSet - HandlerSet + #(#register_set_handler_methods)* + + // register_{name}_handler() - transitions Mj: HandlerNotSet - HandlerSet + #(#register_handler_methods)* + + // offer() is only available when ALL Si = Init, ALL Hi = HandlerSet, ALL Mj = HandlerSet. + impl<#runtime_param_with_bounds> + #validator_name<#runtime_param_name, #(#done_states),*> + { + pub fn offer( + self, + ) -> score_com::Result<<#name<#runtime_param_name> as score_com::Producer<#runtime_param_name>>::OfferedProducer> { + self.producer._offer_internal() + } + } + + // init() - entry point on the original producer, begins the type-state chain. + impl<#runtime_param_with_bounds> #name<#runtime_param_name> { + pub fn init( + self, + ) -> #validator_name<#runtime_param_name, #(#init_states),*> { + #validator_name { + producer: self, + _phantom: core::marker::PhantomData, + } + } + } + }; + + TokenStream::from(expanded) +} From fdbea917cfc11c21fcb0c1d149eae4a300e4dfcf Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Mon, 27 Jul 2026 12:59:40 +0530 Subject: [PATCH 03/25] Rust::com Lola Runtime update for Method APIs * Added impl block for method interface --- .../rust/com-api/com-api-runtime-lola/BUILD | 7 +- .../rust/com-api/com-api-runtime-lola/lib.rs | 7 +- .../com-api/com-api-runtime-lola/method.rs | 108 ++++++++++++++++++ .../com-api/com-api-runtime-lola/runtime.rs | 10 +- 4 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD index 465705984..5685dcb48 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD @@ -15,12 +15,7 @@ load("@rules_rust//rust:defs.bzl", "rust_doc_test", "rust_library", "rust_test") rust_library( name = "com-api-runtime-lola", - srcs = [ - "consumer.rs", - "lib.rs", - "producer.rs", - "runtime.rs", - ], + srcs = glob(["**/*.rs"]), edition = "2024", visibility = ["//score/mw/com:__subpackages__"], deps = [ diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs index 1f9d80df6..4fc031291 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs @@ -25,7 +25,10 @@ //! The crate is structured to facilitate easy integration and usage of the Lola middleware within applications //! that utilize the COM API abstractions. +use core::fmt::Debug; + mod consumer; +mod method; mod producer; mod runtime; @@ -35,4 +38,6 @@ pub use producer::{ }; pub use runtime::{LolaRuntimeImpl, RuntimeBuilderImpl}; -use core::fmt::Debug; +pub use method::{ + LolaMethodCaller, LolaMethodHandler, LolaMethodInArgAllocator, LolaMethodInArgMaybeUninit, +}; diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs new file mode 100644 index 000000000..e077a225e --- /dev/null +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs @@ -0,0 +1,108 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +use core::future::Future; +use score_com_concept::{ + CommData, MethodArgs, MethodArgsAllocate, MethodCaller, MethodHandler, MethodHandlerCall, + MethodInArgAllocator, MethodInArgMaybeUninit, MethodInArgPtr, Result, Runtime, +}; + +pub struct LolaMethodHandler { + _phantom: core::marker::PhantomData<(Args, Return, R)>, +} + +impl MethodHandler + for LolaMethodHandler +{ + fn new(_method_name: &str, _instance_info: R::ProviderInfo) -> Result + where + Self: Sized, + { + Ok(LolaMethodHandler { + _phantom: core::marker::PhantomData, + }) + } + + // This function should have the thread-pool or async executor to handle the incoming method calls and dispatch them to the registered handler. + // For now, we will just have a placeholder implementation. + // So that concurrent method calls can happen on same consumer instance. + // If two consumer call same methods, which may happen then user should have synchronization mechanism in their handler implementation to handle concurrent calls. + fn register_handler(&self, _handler: F) + where + F: MethodHandlerCall, + { + todo!("Implement the logic to register the handler with the underlying system"); + } +} + +pub struct LolaMethodCaller { + _phantom: core::marker::PhantomData<(Args, Return, R)>, +} + +impl MethodCaller + for LolaMethodCaller +{ + fn new(_method_name: &str, _instance_info: R::ConsumerInfo) -> Result + where + Self: Sized, + { + Ok(LolaMethodCaller { + _phantom: core::marker::PhantomData, + }) + } + + fn invoke_with_copy<'a>(&'a self, _args: Args) -> impl Future> + 'a { + async move { todo!("Implement the logic to call the method with copied arguments") } + } + + fn allocate(&self) -> Result<>::UninitTuple> + where + Args: MethodArgsAllocate, + { + todo!("Implement the logic to allocate argument slots using LolaMethodInArgAllocator"); + } + + fn invoke_zero_copy<'a>( + &'a self, + _ptrs: ::PtrTuple, + ) -> impl Future> + 'a { + async move { + todo!("Implement the logic to call the method with pre-allocated argument pointers") + } + } +} + +/// Lola placeholder for a single pre-allocated method argument slot. +pub struct LolaMethodInArgMaybeUninit { + _phantom: core::marker::PhantomData, +} + +impl MethodInArgMaybeUninit for LolaMethodInArgMaybeUninit { + fn write(self, _val: T) -> MethodInArgPtr { + todo!("Implement write into Lola shared-memory slot"); + } + + unsafe fn assume_init(self) -> MethodInArgPtr { + todo!("Implement assume_init for Lola shared-memory slot"); + } +} + +/// Lola placeholder allocator. +pub struct LolaMethodInArgAllocator; + +impl MethodInArgAllocator for LolaMethodInArgAllocator { + type MethodInArgMaybeUninit = LolaMethodInArgMaybeUninit; + fn allocate(&self) -> LolaMethodInArgMaybeUninit { + todo!("Implement allocation from the Lola shared-memory region via &self context"); + } +} diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs index 7d87da820..09cb28d94 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs @@ -16,12 +16,13 @@ use core::marker::PhantomData; use std::path::{Path, PathBuf}; use crate::{ - LolaConsumerDiscovery, LolaConsumerInfo, LolaProducerBuilder, LolaProviderInfo, LolaPublisher, + LolaConsumerDiscovery, LolaConsumerInfo, LolaMethodCaller, LolaMethodHandler, + LolaMethodInArgAllocator, LolaProducerBuilder, LolaProviderInfo, LolaPublisher, LolaSubscribableImpl, }; use score_com_concept::{ - Builder, CommData, FindServiceSpecifier, InstanceSpecifier, Interface, Result, Runtime, - RuntimeBuilder, + Builder, CommData, FindServiceSpecifier, InstanceSpecifier, Interface, MethodArgs, Result, + Runtime, RuntimeBuilder, }; use bridge_ffi_lola::LolaFFIBridge; @@ -36,6 +37,9 @@ impl Runtime for LolaRuntimeImpl { type Subscriber = LolaSubscribableImpl; type ProducerBuilder = LolaProducerBuilder; type Publisher = LolaPublisher; + type MethodInArgAllocator = LolaMethodInArgAllocator; + type MethodCaller = LolaMethodCaller; + type MethodHandler = LolaMethodHandler; type ProviderInfo = LolaProviderInfo; type ConsumerInfo = LolaConsumerInfo; From afad28e9f8f298ffac7512b30f0b2030b4f7a345 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Mon, 27 Jul 2026 13:03:48 +0530 Subject: [PATCH 04/25] Rust::com Update mock runtime for method APIs * Added impl block for method interface traits --- .../com-api/com-api-runtime-mock/runtime.rs | 105 +++++++++++++++++- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs index fceb5b082..a4bc92d34 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs @@ -37,9 +37,10 @@ use std::path::Path; use score_com_concept::{ Builder, CommData, Consumer, ConsumerBuilder, ConsumerDescriptor, FindServiceSpecifier, - InstanceSpecifier, Interface, Producer, ProducerBuilder, ProviderInfo, Publisher, Result, - Runtime, RuntimeBuilder, Sample, SampleContainer, SampleMaybeUninit, SampleMut, - ServiceDiscovery, Subscriber, Subscription, + InstanceSpecifier, Interface, MethodArgs, MethodArgsAllocate, MethodCaller, MethodHandler, + MethodHandlerCall, MethodInArgAllocator, MethodInArgMaybeUninit, MethodInArgPtr, Producer, + ProducerBuilder, ProviderInfo, Publisher, Result, Runtime, RuntimeBuilder, Sample, + SampleContainer, SampleMaybeUninit, SampleMut, ServiceDiscovery, Subscriber, Subscription, }; pub struct MockRuntimeImpl {} @@ -69,6 +70,9 @@ impl Runtime for MockRuntimeImpl { type Subscriber = MockSubscribableImpl; type ProducerBuilder = MockProducerBuilder; type Publisher = MockPublisher; + type MethodInArgAllocator = MockMethodInArgAllocator; + type MethodCaller = MockMethodCaller; + type MethodHandler = MockMethodHandler; type ProviderInfo = MockProviderInfo; type ConsumerInfo = MockConsumerInfo; @@ -507,9 +511,102 @@ impl RuntimeBuilderImpl { } } +pub struct MockMethodHandler { + _phantom: core::marker::PhantomData<(Args, Return, R)>, +} + +impl MethodHandler + for MockMethodHandler +{ + fn new(_method_name: &str, _instance_info: R::ProviderInfo) -> Result + where + Self: Sized, + { + // Implementation for creating a new method handler + Ok(MockMethodHandler { + _phantom: core::marker::PhantomData, + }) + } + + fn register_handler(&self, _handler: F) + where + F: MethodHandlerCall, + { + todo!("Implement the logic to register the handler with the underlying system"); + } +} + +pub struct MockMethodCaller { + _phantom: core::marker::PhantomData<(Args, Return, R)>, +} + +impl MethodCaller + for MockMethodCaller +{ + fn new(_method_name: &str, _instance_info: R::ConsumerInfo) -> Result + where + Self: Sized, + { + Ok(MockMethodCaller { + _phantom: core::marker::PhantomData, + }) + } + + fn invoke_with_copy<'a>(&'a self, _args: Args) -> impl Future> + 'a { + async move { todo!("Implement the logic to call the method with copied arguments") } + } + + fn allocate(&self) -> Result<>::UninitTuple> + where + Args: MethodArgsAllocate, + { + todo!("Implement the logic to allocate method arguments using the MethodInArgAllocator"); + } + + fn invoke_zero_copy<'a>( + &'a self, + _ptrs: ::PtrTuple, + ) -> impl Future> + 'a { + async move { + todo!("Implement the logic to call the method with pre-allocated argument pointers") + } + } +} + +pub struct MockMethodInArgMaybeUninit { + pub _phantom: core::marker::PhantomData, +} + +impl MethodInArgMaybeUninit for MockMethodInArgMaybeUninit { + fn write(self, _val: T) -> MethodInArgPtr { + MethodInArgPtr { + _phantom: core::marker::PhantomData, + } + } + + unsafe fn assume_init(self) -> MethodInArgPtr { + MethodInArgPtr { + _phantom: core::marker::PhantomData, + } + } +} + +pub struct MockMethodInArgAllocator; + +impl MethodInArgAllocator for MockMethodInArgAllocator { + type MethodInArgMaybeUninit = MockMethodInArgMaybeUninit; + fn allocate(&self) -> MockMethodInArgMaybeUninit { + MockMethodInArgMaybeUninit { + _phantom: core::marker::PhantomData, + } + } +} + #[cfg(test)] mod test { - use score_com_concept::{Publisher, SampleContainer, SampleMaybeUninit, SampleMut, Subscription}; + use score_com_concept::{ + Publisher, SampleContainer, SampleMaybeUninit, SampleMut, Subscription, + }; #[test] fn receive_stuff() { From 0af8fe3d4c3b116bfdb6fe1c9f55f7c503abe6b1 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Mon, 27 Jul 2026 13:06:06 +0530 Subject: [PATCH 05/25] Rust::com score_com crate updated for public method interface * Updated import score_com crate --- score/mw/com/rust/score_com.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/score/mw/com/rust/score_com.rs b/score/mw/com/rust/score_com.rs index d16ae15b1..01b2ae5f4 100644 --- a/score/mw/com/rust/score_com.rs +++ b/score/mw/com/rust/score_com.rs @@ -136,8 +136,10 @@ pub use com_api_runtime_lola::RuntimeBuilderImpl as LolaRuntimeBuilderImpl; pub use score_com_concept::{ interface, interface_common, interface_consumer, interface_producer, Builder, CommData, - Consumer, ConsumerBuilder, ConsumerDescriptor, Error, FindServiceSpecifier, InstanceSpecifier, - Interface, OfferedProducer, PlacementDefault, Producer, ProducerBuilder, ProviderInfo, + Consumer, ConsumerBuilder, ConsumerDescriptor, Error, FindServiceSpecifier, HandlerNotSet, + HandlerSet, InstanceSpecifier, Interface, MethodArgs, MethodArgsAllocate, MethodCallInput, + MethodCaller, MethodHandler, MethodHandlerCall, MethodInArgAllocator, MethodInArgMaybeUninit, + MethodInArgPtr, OfferedProducer, PlacementDefault, Producer, ProducerBuilder, ProviderInfo, Publisher, Reloc, Result, Runtime, RuntimeBuilder, SampleContainer, SampleMaybeUninit, SampleMut, ServiceDiscovery, Subscriber, Subscription, }; From 71379813a3da1dd21ad6bd5b207c165977317d74 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Mon, 27 Jul 2026 13:14:38 +0530 Subject: [PATCH 06/25] Rust::com Example app update with Method APIs usage * Added method api usage in example app --- .../com-api-gen/com_api_gen.rs | 36 +++++ .../mw/com/example/com-api-example/src/lib.rs | 4 +- .../com-api-example/src/method_consumer.rs | 129 ++++++++++++++++++ .../com-api-example/src/method_producer.rs | 68 +++++++++ score/mw/com/rust/score_com_concept/BUILD | 3 + .../score_com_concept/interface_macros.rs | 2 +- score/mw/com/rust/score_com_concept/lib.rs | 2 + 7 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 score/mw/com/example/com-api-example/src/method_consumer.rs create mode 100644 score/mw/com/example/com-api-example/src/method_producer.rs diff --git a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs index 99eb1550e..1581c19ff 100644 --- a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs +++ b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs @@ -47,3 +47,39 @@ interface!( exhaust: Event, } ); + +// Example interface definition using the interface macro with a custom UID for the interface. +// This will generate the following types and trait implementations: +// - VehicleMethodsInterface struct with INTERFACE_ID = "VehicleMethodsInterface" +// - VehicleMethodsConsumer, VehicleMethodsProducer, VehicleMethodsOfferedProducer +// with appropriate trait implementations for the VehicleMethods interface. +// As passed methods to macro it will generate the following methods: +// - update_tire_pressure(Tire) -> () +// - update_front_tires_pressure(Tire, Tire) -> () +// - get_tire_pressure() -> Tire +// and this method can be accessed through the consumer instance of VehicleMethodsConsumer. +// Methods use fn-like syntax: method_name(ArgType0, ArgType1, ...) -> ReturnType +// For void return, -> () is required so the macro can identify the member as a method. +interface!( + interface VehicleMethods { + Id = "VehicleMethodsInterface", + update_tire_pressure(Tire) -> (), + update_front_tires_pressure(Tire, Tire) -> (), + get_tire_pressure() -> Tire, + } +); + +// We can also define mix of event , field and method in one interface. +// TODO : Remove the comment once field design PR is merged. +// interface!( +// interface VehicleMonitor { +// Id = "VehicleMonitorInterface", +// left_tire: Event, +// exhaust: Event, +// left_tire_field: Field, +// exhaust_field: Field, +// update_tire_pressure(Tire) -> (), +// update_front_tires_pressure(Tire, Tire) -> (), +// get_tire_pressure() -> Tire, +// } +// ); diff --git a/score/mw/com/example/com-api-example/src/lib.rs b/score/mw/com/example/com-api-example/src/lib.rs index fb4e37ff7..293780e53 100644 --- a/score/mw/com/example/com-api-example/src/lib.rs +++ b/score/mw/com/example/com-api-example/src/lib.rs @@ -12,12 +12,14 @@ ********************************************************************************/ pub mod consumer; +pub mod method_consumer; +pub mod method_producer; pub mod producer; pub use consumer::VehicleMonitorConsumer; pub use producer::VehicleMonitorProducer; -use score_com::{Interface, Producer}; use com_api_gen::VehicleInterface; +use score_com::{Interface, Producer}; // Type aliases for generated consumer and offered producer types for the Vehicle interface // VehicleConsumer is the consumer type generated for the Vehicle interface, parameterized by the runtime R diff --git a/score/mw/com/example/com-api-example/src/method_consumer.rs b/score/mw/com/example/com-api-example/src/method_consumer.rs new file mode 100644 index 000000000..fd21a0dad --- /dev/null +++ b/score/mw/com/example/com-api-example/src/method_consumer.rs @@ -0,0 +1,129 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// This file demonstrate the usage of consumer method APIs, which are generated for the VehicleMethodsInterface. +// It shows how method can be called using copy and zero-copy arguments, +// And async method call can be awaited to get the result. + +// Notes: we are creating consumer instance specific for method here but this is just for demonstration perpose, +// for same consumer insatnce method / event/ field can be consume as per offer interface. +// This can not be used or called in main of example app, as runtime implementation is not available for method APIs. + +#![allow(unused)] + +use score_com::{ + Builder, FindServiceSpecifier, InstanceSpecifier, Interface, MethodCaller, + MethodInArgMaybeUninit, Runtime, ServiceDiscovery, +}; + +use com_api_gen::{Tire, VehicleMethodsInterface}; + +type VehicleMethodConsumer = ::Consumer; + +// These functions are just to demonstrate the method APIs, and they can not be used in main of example app, +// as runtime implementation is not available for method APIs. +fn create_consumer_method( + runtime: &R, + service_id: InstanceSpecifier, +) -> VehicleMethodConsumer { + let consumer_discovery = + runtime.find_service::(FindServiceSpecifier::Specific(service_id)); + let available_service_instances = consumer_discovery + .get_available_instances() + .expect("Failed to get available service instances"); + + // Select service instance at specific handle_index + let handle_index = 0; // or any index you need from vector of instances + let consumer_builder = available_service_instances + .into_iter() + .nth(handle_index) + .expect("Failed to get consumer builder at specified handle index"); + + consumer_builder + .build() + .expect("Failed to build consumer instance") +} + +// Method calls return `impl Future>`, so they must be `.await`ed. +// All the method called is async. + +// Copy path: single positional argument. +// Demonstrates calling a method with a single argument, where the argument is copied into the method call. +// Zero-copy path: allocate, write, then call the method with allocaed args. +async fn consumer_method_processing(consumer: VehicleMethodConsumer) { + // Copy path: single positional argument — no tuple needed. + let tire = Tire { pressure: 30.0 }; + match consumer.update_tire_pressure(tire).await { + Ok(_) => println!("Successfully called update_tire_pressure method"), + Err(e) => eprintln!("Failed to call update_tire_pressure method: {:?}", e), + } + + let (uninit1,) = consumer + .update_tire_pressure + .allocate() + .expect("Failed to allocate method arguments"); + let tire1ptr = uninit1.write(Tire { pressure: 35.0 }); + + // Zero-copy path: allocate, write, then call the same wrapper. + match consumer.update_tire_pressure(tire1ptr).await { + Ok(_) => println!("Successfully called update_tire_pressure method with allocated args"), + Err(e) => eprintln!( + "Failed to call update_tire_pressure method with allocated args: {:?}", + e + ), + } +} + +// Get Method call which has no argument and return a value, which is also async. +async fn method_get_call(consumer: VehicleMethodConsumer) { + // Copy path: zero-argument method — empty parens, no empty-tuple needed. + futures::executor::block_on(async { + match consumer.get_tire_pressure().await { + Ok(tire) => println!("Current tire pressure: {:?}", tire), + Err(e) => eprintln!("Failed to call get_tire_pressure method: {:?}", e), + } + }); +} + +//two arguments method. +// It demonstrates calling a method with two arguments, where the arguments are copied into the method call. +// It also demonstrates the zero-copy path, where the arguments are allocated, written, and then passed to the method call. +async fn consumer_processing(consumer: VehicleMethodConsumer) { + // Copy path: two arguments method. + let tire1 = Tire { pressure: 31.0 }; + let tire2 = Tire { pressure: 32.0 }; + match consumer.update_front_tires_pressure(tire1, tire2).await { + Ok(_) => println!("Successfully called update_front_tires_pressure method"), + Err(e) => eprintln!("Failed to call update_front_tires_pressure method: {:?}", e), + } + + let (uninit1, uninit2) = consumer + .update_front_tires_pressure + .allocate() + .expect("Failed to allocate method arguments"); + let tire1ptr = uninit1.write(Tire { pressure: 36.0 }); + let tire2ptr = uninit2.write(Tire { pressure: 37.0 }); + // Zero-copy path: allocate, write both args, then call the same method with allocated args. + match consumer + .update_front_tires_pressure(tire1ptr, tire2ptr) + .await + { + Ok(_) => { + println!("Successfully called update_front_tires_pressure method with allocated args") + } + Err(e) => eprintln!( + "Failed to call update_front_tires_pressure method with allocated args: {:?}", + e + ), + } +} diff --git a/score/mw/com/example/com-api-example/src/method_producer.rs b/score/mw/com/example/com-api-example/src/method_producer.rs new file mode 100644 index 000000000..41ba44105 --- /dev/null +++ b/score/mw/com/example/com-api-example/src/method_producer.rs @@ -0,0 +1,68 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// This file demonstrate the usage of producer method APIs, which are generated for the VehicleMethodsInterface. + +// Notes: we are creating producer instance specific for method here but this is just for demonstration perpose, +// for same producer insatnce method / event/ field can be offered as per offer interface. + +#![allow(unused)] + +use score_com::{Builder, InstanceSpecifier, Interface, Producer, Runtime}; + +use com_api_gen::{Tire, VehicleMethodsInterface}; + +type VehicleMethodOfferedProducer = + <::Producer as Producer>::OfferedProducer; + +// These functions are just to demonstrate the method APIs, and they can not be used in main of example app, +// as runtime implementation is not available for method APIs. +// Once producer instance is created, it must be registered with method handlers. +// As this can be called concurrently from runtime, the user needs to handle the synchronization of data if required. +// The method handlers are registered using the `register__handler` methods on the producer instance. +// The handlers are registered before offering the producer instance, so that the consumer can call the methods on the producer instance. +// If user call `producer.offer()` before registering the handlers, it will panic, as handlers are not registered yet. +// And if user cann the `producer.init()` but forget to register one of method halder then complier will give error, +// as offer method using `init()` will require all method handlers to be registered before offering the producer instance. +fn create_producer_method( + runtime: &R, + service_id: InstanceSpecifier, +) -> VehicleMethodOfferedProducer { + let producer_builder = runtime.producer_builder::(service_id); + let producer = producer_builder + .build() + .expect("Failed to build producer instance"); + producer + .init() + // register method handler like function pointer. + .register_update_tire_pressure_handler(process_left_tire) + .register_get_tire_pressure_handler(|| { + println!("Received get_tire_pressure call"); + // Return a sample tire pressure value, just dummy value returned for demonstration + Tire { pressure: 32.0 } + }) + .register_update_front_tires_pressure_handler(|tire1: Tire, tire2: Tire| { + println!( + "Received update_front_tires_pressure call with tire1: {:?}, tire2: {:?}", + tire1, tire2 + ); + () + }) + .offer() + .expect("Failed to offer producer instance") +} + +fn process_left_tire(tire: Tire) { + // do some processing with the tire data + println!("Processing left tire pressure: {:?}", tire); +} diff --git a/score/mw/com/rust/score_com_concept/BUILD b/score/mw/com/rust/score_com_concept/BUILD index 2f557d48e..70443f8cf 100644 --- a/score/mw/com/rust/score_com_concept/BUILD +++ b/score/mw/com/rust/score_com_concept/BUILD @@ -57,5 +57,8 @@ rust_unit_test( name = "score_com_concept-macros-unit-tests", srcs = ["interface_macros.rs"], features = ["link_std_cpp_lib"], + # TODO: uncomment this once field or method one PR is merged, + # Unit test failed because macro has field and method both types + tags = ["manual"], deps = ["//score/mw/com/rust:score_com"], ) diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_macros.rs index 757004136..341195b94 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -542,7 +542,7 @@ macro_rules! interface_producer_mixed { // Producer struct - derives TypeStateValidator for compile-time offer() gating. // Fields: FieldPublisher per field + MethodHandler per method. // Event publishers are NOT stored here; they are created during _offer_internal(). - #[derive($crate::score_com_concept_macros::TypeStateValidator)] + #[derive($crate::score_com_macros::TypeStateValidator)] pub struct [<$id Producer>] { $( $fi_name: R::FieldPublisher<$fi_type>, diff --git a/score/mw/com/rust/score_com_concept/lib.rs b/score/mw/com/rust/score_com_concept/lib.rs index c316f3053..4c7c01b2c 100644 --- a/score/mw/com/rust/score_com_concept/lib.rs +++ b/score/mw/com/rust/score_com_concept/lib.rs @@ -32,4 +32,6 @@ pub use interface_macros::{HandlerNotSet, HandlerSet}; pub use method_concept::*; #[doc(hidden)] pub use paste; +#[doc(hidden)] +pub use score_com_macros; pub use reloc::Reloc; From f9abcf482fbece6366ac630d85e8acfed8277f6e Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Mon, 27 Jul 2026 15:22:42 +0530 Subject: [PATCH 07/25] Rust::com Crate documentation Update * Updated concept crate documentation * Updated interface macro document * Update type state macro document --- .../rust/com-api/com-api-runtime-lola/lib.rs | 3 + .../com-api/com-api-runtime-lola/method.rs | 16 +++-- score/mw/com/rust/score_com.rs | 6 ++ .../score_com_concept/interface_macros.rs | 58 ++++++++++++------ .../method_arities_macros.rs | 18 ++---- .../rust/score_com_concept/method_concept.rs | 61 ++++++++++--------- score/mw/com/rust/score_com_macros/lib.rs | 28 ++++++--- .../score_com_macros/type_state_validator.rs | 11 +++- 8 files changed, 127 insertions(+), 74 deletions(-) diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs index 4fc031291..b3e38d8d1 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs @@ -28,6 +28,9 @@ use core::fmt::Debug; mod consumer; +// Note: The `method` module is currently a placeholder and +// will be implemented in the future for the Lola runtime. +// https://github.com/eclipse-score/communication/issues/782 mod method; mod producer; mod runtime; diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs index e077a225e..b158facfd 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs @@ -11,6 +11,12 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +// TODO: https://github.com/eclipse-score/communication/issues/782 +// Implement the method related interface for Lola runtime. This is a placeholder implementation for now. +// also implement required FFI interface to call the Lola runtime for method calls and registration of handlers. + +/// All the struct and trait implementations are placeholders for now, +/// and will be implemented in future as per the requirements of the Lola runtime. use core::future::Future; use score_com_concept::{ CommData, MethodArgs, MethodArgsAllocate, MethodCaller, MethodHandler, MethodHandlerCall, @@ -33,10 +39,12 @@ impl MethodHandler(&self, _handler: F) where F: MethodHandlerCall, @@ -82,7 +90,7 @@ impl MethodCaller { _phantom: core::marker::PhantomData, } @@ -97,7 +105,7 @@ impl MethodInArgMaybeUninit for LolaMethodInArgMaybeUninit { } } -/// Lola placeholder allocator. +// Lola placeholder allocator. pub struct LolaMethodInArgAllocator; impl MethodInArgAllocator for LolaMethodInArgAllocator { diff --git a/score/mw/com/rust/score_com.rs b/score/mw/com/rust/score_com.rs index 01b2ae5f4..83f213c02 100644 --- a/score/mw/com/rust/score_com.rs +++ b/score/mw/com/rust/score_com.rs @@ -130,6 +130,12 @@ //! # Further reading //! - `score_com_concept` crate — trait definitions and full API documentation //! - `doc/high_level_design_detail.md` — internal architecture and layer details +//! +//! Note: Event APIs are fully supported with the Lola runtime. +//! Method API traits exported from this crate reflect the intended design but are not yet backed +//! by a Lola runtime implementation in Rust side, they serve as a demonstration of the planned interface. +//! Method APIs developmement is tracked in +//! https://github.com/eclipse-score/communication/issues/782 pub use com_api_runtime_lola::LolaRuntimeImpl; pub use com_api_runtime_lola::RuntimeBuilderImpl as LolaRuntimeBuilderImpl; diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_macros.rs index 341195b94..2091f9d80 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -10,25 +10,37 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -// Type-state marker for handler not registered (compile-time tracking). +/// Type-state marker for uninitialized field value state (compile-time tracking). +/// +/// These marker types are never constructed as values - they only appear as generic +/// type parameters inside `PhantomData<(S, H)>` on the generated `{Id}Validator` struct +/// (see `TypeStateValidator` in `score_com_macros`). The compiler's `dead_code` lint +/// flags unit structs that are never instantiated, so it is suppressed here deliberately. #[allow(dead_code)] pub struct Uninit; -/// Type-state marker for initialized field state (compile-time tracking). +/// Type-state marker for initialized field value state (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. #[allow(dead_code)] pub struct Init; /// Type-state marker for handler not registered (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. #[allow(dead_code)] pub struct HandlerNotSet; /// Type-state marker for handler registered (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. #[allow(dead_code)] pub struct HandlerSet; /// Main interface macro that generates Consumer, Producer, and OfferedProducer types /// along with all necessary trait implementations. /// +/// Supports Event-only interfaces (backward compatible) and mixed interfaces containing +/// any combination of `Event`, `Field`, and `method_name(Args) -> Return` members +/// in the same definition block. +/// /// Automatically generates unique type names from the identifier of macro invocation. /// For an interface with identifier `{id}`, it generates: /// - `{id}Interface` - Struct representing the interface with INTERFACE_ID constant @@ -76,7 +88,7 @@ pub struct HandlerSet; /// interface Vehicle { /// Id = "AbcInterface", /// left_tire: Event, -/// left_tire_field: Field, +/// left_tire_field: Field, /// left_tire_method(Tire) -> Tire, /// } /// ); @@ -91,17 +103,29 @@ pub struct HandlerSet; /// `left_tire_method: MethodHandler<(Tire,), Tire>`. Requires `.init()` chain before `.offer()`. /// - `VehicleOfferedProducer` with event publisher `left_tire`, plus moved field publisher and /// method handler. -/// Main interface macro that supports Event-only interfaces (backward compatible) and -/// mixed interfaces containing any combination of `Event`, `Field`, and -/// `method_name(Args) -> Return` members in the same definition block. +/// - For `left_tire_field`, the user needs to both update the initial value and register the +/// set-handler callback, using the same `init()` chain, before offering the producer instance. /// -/// # Backward-compatible arms (unchanged) -/// Event-only interfaces continue to work without any changes. +/// The code will look like this: +/// ```ignore +/// let producer = producer_builder.build().expect("Failed to build producer instance"); +/// producer.init() +/// .update_left_tire_field(&initial_value)? +/// .register_set_handler_left_tire_field(|value| { +/// println!("Received left_tire_field update: {:?}", value); +/// }) +/// .register_left_tire_method_handler(|tire: Tire| { +/// println!("Received left_tire_method call with tire: {:?}", tire); +/// tire +/// }) +/// .offer()?; +/// ``` +/// In the code above, if the user forgets to register the field set-handler or the method +/// handler, it will be a compile-time error, since `init()` requires all handlers to be +/// registered before `offer()` becomes available. /// -/// # Mixed / unified arms -/// When the body contains anything other than a homogeneous list of `Event` members -/// (i.e., any `Field` or fn-like method member), the recursive-macro arms parse the body -/// and delegate to `interface_consumer_mixed!` / `interface_producer_mixed!`. +/// If the user calls `producer.offer()` directly (without going through `init()`), it will +/// panic at runtime, since the handlers have not been registered yet. #[macro_export] macro_rules! interface { // Backward-compatible: Event-only, auto-generated ID @@ -347,7 +371,7 @@ macro_rules! interface_consumer { }; } -/// This is Event specific. +/// This is Event specific. /// Macro to implement the Producer and OfferedProducer traits for /// a given interface ID and its events. /// Generates Producer and OfferedProducer structs with publishers for each event. @@ -505,17 +529,17 @@ macro_rules! interface_consumer_mixed { /// interfaces that may contain any combination of events, fields, and methods. /// /// # Design -/// - **Event publishers** (`R::Publisher`) are created *lazily during `_offer_internal()`* +/// - Event publishers (`R::Publisher`) are created *lazily during `_offer_internal()`* /// so they are only present on the `OfferedProducer`. -/// - **Field publishers** (`R::FieldPublisher`) are created eagerly in `Producer::new()` and +/// - Field publishers (`R::FieldPublisher`) are created eagerly in `Producer::new()` and /// moved into `OfferedProducer` when the service is offered. -/// - **Method handlers** (`R::MethodHandler`) likewise created eagerly and moved. +/// - Method handlers (`R::MethodHandler`) likewise created eagerly and moved. /// /// When the interface has at least one field or method member, the `Producer` struct derives /// `TypeStateValidator` which generates the `.init()` entry point and the `update_*` / /// `register_set_handler_*` / `register_*_handler` chain required before `offer()`. /// -/// When the interface has *only* events (no fields, no methods), a plain `offer()` is generated +/// When the interface has only events (no fields, no methods), a plain `offer()` is generated /// directly (matching the existing event-only pattern). #[doc(hidden)] #[macro_export] diff --git a/score/mw/com/rust/score_com_concept/method_arities_macros.rs b/score/mw/com/rust/score_com_concept/method_arities_macros.rs index f0078025b..68800e1ac 100644 --- a/score/mw/com/rust/score_com_concept/method_arities_macros.rs +++ b/score/mw/com/rust/score_com_concept/method_arities_macros.rs @@ -21,27 +21,17 @@ //! `Reloc`, `CommData`, `MethodArgs`, `MethodArgsAllocate`, `MethodCallInput` (zero-copy //! path), and `MethodHandlerCall` - is generated using macros. //! -//! `_gen_method_wrapper!` in `interface_macros.rs` self-generates its argument +//! Note: `_gen_method_wrapper!` in `interface_macros.rs` self-generates its argument //! identifiers via a counting recursive macro, so it has no separate limit to keep in //! sync - raising the arity here is the only change needed. //! (I don't think this many arguments will support by clippy linting, -//! so we may need to reduce the limit to 8 or 10 in the future based on project clippy linting rules.) +//! so we may need to reduce the limit to 4 to 5 in the future based on project clippy linting rules.) //! //! # Arity 0 special case //! //! Arity 0 (`()`) is handled separately in `com_api_method.rs` because the zero-tuple -//! has no positional variables to destructure. This macro covers arities **1 and above**. -//! -//! # How the recursive macro works -//! -//! The macro maintains two accumulated token lists in parallel: -//! - **Type vars** `[T1, T2, …]` - used as generic parameters in trait impls. -//! - **Arg names** `[a0, a1, …]` - used for positional destructuring inside -//! `MethodHandlerCall::call` and `MethodCallInput::invoke`. -//! -//! At each step the next `(TypeIdent, arg_ident)` pair is peeled from the input, the -//! two accumulated lists grow by one, all six impls for the new arity are emitted, and -//! the recursion continues with the extended lists. +//! has no positional variables to destructure. +//! This macro covers arities 1 through 8 (inclusive) by default, but can be extended to higher arities if needed. use crate::{ CommData, MethodArgs, MethodArgsAllocate, MethodCallInput, MethodCaller, MethodHandlerCall, diff --git a/score/mw/com/rust/score_com_concept/method_concept.rs b/score/mw/com/rust/score_com_concept/method_concept.rs index fc36cc8a8..b84d5fca2 100644 --- a/score/mw/com/rust/score_com_concept/method_concept.rs +++ b/score/mw/com/rust/score_com_concept/method_concept.rs @@ -12,18 +12,17 @@ ********************************************************************************/ /// For method as rust side does not have any varadic function argument support, -/// so we are having tuple of arguments, so we can have any number of arguments (currently up to 2) without any extra boilerplate. +/// we are having tuple of arguments, so we can have any number of arguments (currently up to 8) without any extra boilerplate. /// we have implemeted blanket implementation of MethodArgs, MethodArgsAllocate and -/// MethodCallInput traits for all supported arities (0–2 arguments) are provided in this crate. +/// MethodCallInput traits for all supported arities (0–8 arguments) using `impl_all_arities!` macro in this crate. /// This blanket implementation help in design to have any number of arguments -/// (currently up to 2) without runtime specific implementation for each arity. +/// (currently up to 8) without runtime specific implementation for each arity. /// This crate provides the necessary traits and types to support method calls in a communication API, /// including handling of method arguments, allocation of uninitialized argument, /// and invocation of methods with both copy and zero-copy semantics. -/// Which enable the interface macro to generate exactly a single consumer method per interface method - -/// instead of two separate copy and zero-copy methods. +/// /// We want to follow the same semantics for method like c++ provide for method call, -/// and because of that we have added few supporting traits which help to create similar semantics for method call in rust side. +/// and because of that we have added few supporting traits which help to create similar semantics for method call in rust side. /// In the event and field we have `SampleMut` with that allocated memory can call send, /// but in method call we can not use that approach as we do not have any common API to call send/update, /// Method takes the argument whether it is by value or by zero-copy in same method function/method, @@ -31,17 +30,17 @@ /// Which is not user facing APIs but used by interface macro and supporting traits. /// /// trait details: -/// MethodHandler: Producer side registration of method handlers, +/// `MethodHandler`: Producer side registration of method handlers, /// this needs to be implemented by runtime for producer side method handler registration. -/// MethodCaller: Consumer side caller of methods, this needs to be implemented by runtime for consumer side method calls. +/// `MethodCaller`: Consumer side caller of methods, this needs to be implemented by runtime for consumer side method calls. /// This trait provides methods for invoking methods with both copy and zero-copy semantics, /// also handles allocation of uninitialized method arguments for zero-copy calls, /// this is user facing API for consumer side method calls. /// This trait is used by the interface macro to generate consumer methods for each interface method and /// invoke the runtime specific method caller implementation. -/// MethodInArgMaybeUninit: This is the uninitialized type for a single method argument, +/// `MethodInArgMaybeUninit`: This is the uninitialized type for a single method argument, /// it is used in the zero-copy method call path. -/// MethodInArgAllocator: This is for runtime-specific method argument allocation, +/// `MethodInArgAllocator`: This is for runtime-specific method argument allocation, /// it is used in the zero-copy method call path. /// Which provide the allocate API for specific argument type and /// return the uninitialized method argument type for that argument type. @@ -49,27 +48,35 @@ /// Now below traits are marker / marker-like (because it is implemented for all supported arities) traits and /// which no need to implement by runtime because blanket implementation is added in this crate. /// -/// MethodArgs: Marker trait for method argument tuples, +/// `MethodArgs`: Marker trait for method argument tuples, /// this is used to carry the matching tuple of MethodInArgPtr used in the zero-copy call path. -/// MethodArgsAllocate: Maps an Args tuple type to the matching uninitialized method argument tuple for a specific runtime allocator A, +/// `MethodArgsAllocate`: Maps an Args tuple type to the matching uninitialized method argument tuple for a specific runtime allocator A, /// this is used to produce the uninitialized method arguments for zero-copy method call path. -/// MethodCallInput: Unified input for a method call accepted by the interface macro-generated consumer methods, +/// `MethodCallInput`: Unified input for a method call accepted by the interface macro-generated consumer methods, /// this is used to dispatch the method call to the appropriate runtime specific method - /// caller implementation based on the type of the input arguments. -/// MethodHandlerCall: Callable handler function for a method with Args inputs and Return output, +/// `MethodHandlerCall`: Callable handler function for a method with Args inputs and Return output, /// this is used to register the handler function for a method on the producer side, /// which can be a plain closure or any FnMut with the matching signature, /// and it will automatically satisfy this trait, /// so that the interface macro can generate the necessary code to register the handler function for each interface method on the producer side. /// +/// We have method_arities_macros.rs which is used to generate the blanket implementation of MethodArgs, MethodArgsAllocate and +/// MethodCallInput traits for all supported arities (0–8 arguments) are provided in this crate. +/// This macro also extend the `Reloc` and `CommData` traits for all supported arities (0–8 arguments) are provided in this crate. +/// Macro invocation happen in same file so no need to invoke from user side or runtime side. +/// Also we can not depend on `interface_macros` because this macro code expand in library crate and interface macro is used in user crate, +/// and library should be build before user crate, so we can not depend on that. +/// // TODO: Add a blocking `.wait()` convenience for method-call futures, for sync callers who don't -// want to bring their own async executor (similar in spirit to `futures::executor::block_on`). +// want to bring their own async executor (similar to `futures::executor::block_on`). use crate::concept::*; use core::future::Future; // This is a pointer type for a pre-allocated method argument. It is used in the zero-copy method call path. // TODO: Remove this once memory layout implementation is added in rust side, same like samplePtr. // Also need to check about lifetime of this pointer and add all the trait or type which is required. +// https://github.com/eclipse-score/communication/issues/781 pub struct MethodInArgPtr { pub _phantom: core::marker::PhantomData, } @@ -92,7 +99,8 @@ pub trait MethodHandler /// which is automatically satisfied by any function or closure with the appropriate signature. /// /// # Arguments - /// * `handler` - The handler function to register for the method, which has to bound the `MethodHandlerCall` trait blanket implementation. + /// * `handler` - The handler function to register for the method, + /// which has to bound the `MethodHandlerCall` trait blanket implementation. fn register_handler(&self, handler: F) where F: MethodHandlerCall; @@ -101,7 +109,8 @@ pub trait MethodHandler /// Consumer side caller of methods. /// This is the interface that a consumer implements to call methods on a producer. /// In this trait we have two methods for method call, one is `invoke_with_copy` and another is `invoke_zero_copy`, -/// Which are not intended to be used by user directly, but used by interface macro to generate consumer methods for each interface method. +/// Which are not intended to be used by user directly, +/// but used by interface macro to generate consumer methods for each interface method. /// This used by runtime to implement the specific implementation for method call. /// Both call methods return a future so callers can `.await` the result. pub trait MethodCaller { @@ -132,9 +141,12 @@ pub trait MethodCaller /// Note: This method returns the tuple of uninitialized method arguments for the given `Args` type, /// which can then be written individually and passed to the method. /// Here `Args` is a tuple of method argument types for the given method, - /// and `UninitTuple` is the corresponding tuple of uninitialized method argument types for the given runtime's method argument allocator. - /// e.g., for a method with signature `fn my_method(arg1: T1, arg2: T2) -> Return`, the `Args` type would be `(T1, T2)`, - /// and the `UninitTuple` type would be `(A::MethodInArgMaybeUninit, A::MethodInArgMaybeUninit)` where `A` is the runtime's method argument allocator. + /// and `UninitTuple` is the corresponding tuple of uninitialized method argument types for + /// the given runtime's method argument allocator. + /// e.g., for a method with signature `fn my_method(arg1: T1, arg2: T2) -> Return`, + /// the `Args` type would be `(T1, T2)`, + /// and the `UninitTuple` type would be `(A::MethodInArgMaybeUninit, A::MethodInArgMaybeUninit)` + /// where `A` is the runtime's method argument allocator. fn allocate( &self, ) -> Result<>::UninitTuple> @@ -162,8 +174,6 @@ pub trait MethodCaller /// API does not enforce at compile time that all method arguments in the tuple are written before /// method is called. A user can call `assume_init()` on an unwritten method argument, which /// is undefined behaviour once real shared memory backs these method arguments. -/// -/// TODO: We can consider adding a typesatate or builder pattern to enforce this, if required. pub trait MethodInArgMaybeUninit { /// Write a value into this pre-allocated method argument and return the initialized pointer. fn write(self, val: T) -> MethodInArgPtr; @@ -287,13 +297,6 @@ where /// /// Application code on the producer side passes a plain closure to `MethodHandler::register_handler`; /// any `Fn` with the matching signature automatically satisfies this trait. -/// -/// `Fn` (immutable receiver) is required rather than `FnMut` because the runtime may dispatch -/// concurrent calls from a thread pool. -/// TODO: We can think about adding `FnMut` support in the future, -/// but it would require a synchronization mechanism in the runtime to ensure that concurrent calls do not violate the `FnMut` contract. -/// So this can be decided at the implementation time of the runtime, whether it wants to support `FnMut` or not. -/// /// Runtimes do not implement this trait. /// Blanket impls for all supported arities are provided in this crate so that closures just work without any extra boilerplate. pub trait MethodHandlerCall: Send + Sync + 'static { diff --git a/score/mw/com/rust/score_com_macros/lib.rs b/score/mw/com/rust/score_com_macros/lib.rs index 8cfba9ab6..0edf50166 100644 --- a/score/mw/com/rust/score_com_macros/lib.rs +++ b/score/mw/com/rust/score_com_macros/lib.rs @@ -338,11 +338,22 @@ fn collect_field_types(data: &Data) -> Result, ()> { } /// Unified derive macro for compile-time type-state validation of Field and Method producers. -/// -/// Detects member types by the last segment of each field's type path: -/// - `FieldPublisher` → generates `update_{name}()` and `register_set_handler_{name}()` -/// - `MethodHandler` → generates `register_{name}_handler()` +/// It generate the validator struct and the type-state chain for the producer, +/// ensuring that all required fields and handlers are properly set before offering the service. +/// User need to call `init()` on the producer to start the type-state chain, and then call the generated +/// `update_*` and `register_set_handler_*`, `register_*_handler` methods in any order, +/// and finally call `offer()` to complete the chain. +/// user will get compile-time error if any required field or handler is not set before calling `offer()`. +/// +/// User no need to use this macro explicitly, +/// it will be automatically generated by the `interface!` macro for the producer struct. +/// +/// Note: This macro identifies member types by the last segment of each field's type path: +/// - `FieldPublisher` - generates `update_{name}()` and `register_set_handler_{name}()` +/// - `MethodHandler` - generates `register_{name}_handler()` /// - `instance_info` field is always skipped. +/// So if member type is changed to a different type or renamed, +/// then macro need to be updated to recognize the new type name or path segment. /// /// # Generated validator struct /// @@ -353,12 +364,12 @@ fn collect_field_types(data: &Data) -> Result, ()> { /// /// `offer()` is only available when ALL `Si = Init`, ALL `Hi = HandlerSet`, ALL `Mj = HandlerSet`. /// -/// Entry point on the producer: `init()` — begins the type-state chain. +/// Entry point on the producer: `init()` - begins the type-state chain. /// /// Degenerates correctly: -/// - Field-only struct → no `Mj` params -/// - Method-only struct → no `Si`/`Hi` params -/// - Mixed struct → all param groups combined +/// - Field-only struct - no `Mj` params +/// - Method-only struct - no `Si`/`Hi` params +/// - Mixed struct - all param groups combined /// /// # Usage /// @@ -376,6 +387,7 @@ fn collect_field_types(data: &Data) -> Result, ()> { /// // .offer()? /// ``` // TODO: Document tests need to be added for this macro, including successful and failed compilation cases. +// Once field or method design merged, other PR can add the tests for this macro. #[proc_macro_derive(TypeStateValidator)] pub fn derive_typestate_validator(input: TokenStream) -> TokenStream { type_state_validator::derive_typestate_validator_impl(input) diff --git a/score/mw/com/rust/score_com_macros/type_state_validator.rs b/score/mw/com/rust/score_com_macros/type_state_validator.rs index f608a19a2..7dac28567 100644 --- a/score/mw/com/rust/score_com_macros/type_state_validator.rs +++ b/score/mw/com/rust/score_com_macros/type_state_validator.rs @@ -37,6 +37,9 @@ use syn::{parse_macro_input, Data, DeriveInput, Fields, Type}; /// /// Entry point on the producer: `init()` - returns the validator with every state /// parameter set to its initial value (`Uninit` / `HandlerNotSet`). +/// +/// Note: This macro identifies member types by the member types so if member type is changed to a different type or renamed, +/// then macro need to be updated to recognize the new type name or path segment. pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; @@ -67,6 +70,7 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { .into(); } }, + // TODO: If require support for enum or tuple struct then add support here. _ => { return syn::Error::new_spanned(name, "TypeStateValidator only supports structs") .to_compile_error() @@ -95,11 +99,13 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { Some(i) => i.clone(), None => continue, }; - // Skip the bookkeeping field — it carries no type-state. + // Skip the `instance_info` field, which is not part of the type-state validation. if ident == "instance_info" { continue; } + // Note: pattern matching ("FieldPublisher", "MethodHandler") must match the trait/type + // names used in the Runtime associated types. If those names change, update here too. if let Type::Path(type_path) = &f.ty { if let Some(segment) = type_path.path.segments.last() { match segment.ident.to_string().as_str() { @@ -135,7 +141,8 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { } } } - + // If no FieldPublisher or MethodHandler members were found, emit a compile error. + // because macro is only added to producer struct which has at least one FieldPublisher or MethodHandler member. if field_members.is_empty() && method_members.is_empty() { return syn::Error::new_spanned( name, From 01e0831f4187cea27cbb998263da249ae0a0027d Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Mon, 27 Jul 2026 22:43:08 +0530 Subject: [PATCH 08/25] Rust::com Update the return value of Methods * Updated return value to MethodReturnSample --- .../com-api-gen/com_api_gen.rs | 3 ++- .../com-api-example/src/method_consumer.rs | 6 +++-- .../rust/com-api/com-api-runtime-lola/lib.rs | 1 + .../com-api/com-api-runtime-lola/method.rs | 20 +++++++++++++++-- .../com-api/com-api-runtime-lola/runtime.rs | 5 +++-- .../com-api/com-api-runtime-mock/runtime.rs | 22 +++++++++++++++++-- .../mw/com/rust/score_com_concept/concept.rs | 5 +++++ .../score_com_concept/interface_macros.rs | 8 +++---- .../method_arities_macros.rs | 2 +- .../rust/score_com_concept/method_concept.rs | 22 ++++++++++--------- 10 files changed, 70 insertions(+), 24 deletions(-) diff --git a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs index 1581c19ff..8e6b78f2d 100644 --- a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs +++ b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs @@ -58,7 +58,8 @@ interface!( // - update_front_tires_pressure(Tire, Tire) -> () // - get_tire_pressure() -> Tire // and this method can be accessed through the consumer instance of VehicleMethodsConsumer. -// Methods use fn-like syntax: method_name(ArgType0, ArgType1, ...) -> ReturnType +// Methods use fn-like syntax: +// method_name(ArgType0, ArgType1, ...) -> score_com::Result>. // For void return, -> () is required so the macro can identify the member as a method. interface!( interface VehicleMethods { diff --git a/score/mw/com/example/com-api-example/src/method_consumer.rs b/score/mw/com/example/com-api-example/src/method_consumer.rs index fd21a0dad..7fc1bffaf 100644 --- a/score/mw/com/example/com-api-example/src/method_consumer.rs +++ b/score/mw/com/example/com-api-example/src/method_consumer.rs @@ -88,14 +88,16 @@ async fn consumer_method_processing(consumer: VehicleMethodConsumer< async fn method_get_call(consumer: VehicleMethodConsumer) { // Copy path: zero-argument method — empty parens, no empty-tuple needed. futures::executor::block_on(async { + // it returns a `Result>` + // which is a wrapper around the return value of the method call. match consumer.get_tire_pressure().await { - Ok(tire) => println!("Current tire pressure: {:?}", tire), + Ok(tire) => println!("Current tire pressure: {:?}", *tire), Err(e) => eprintln!("Failed to call get_tire_pressure method: {:?}", e), } }); } -//two arguments method. +// two arguments method. // It demonstrates calling a method with two arguments, where the arguments are copied into the method call. // It also demonstrates the zero-copy path, where the arguments are allocated, written, and then passed to the method call. async fn consumer_processing(consumer: VehicleMethodConsumer) { diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs index b3e38d8d1..391a2c91b 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs @@ -43,4 +43,5 @@ pub use runtime::{LolaRuntimeImpl, RuntimeBuilderImpl}; pub use method::{ LolaMethodCaller, LolaMethodHandler, LolaMethodInArgAllocator, LolaMethodInArgMaybeUninit, + LolaMethodReturnSample, }; diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs index b158facfd..ef17e0db5 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs @@ -18,11 +18,27 @@ /// All the struct and trait implementations are placeholders for now, /// and will be implemented in future as per the requirements of the Lola runtime. use core::future::Future; +use core::ops::Deref; use score_com_concept::{ CommData, MethodArgs, MethodArgsAllocate, MethodCaller, MethodHandler, MethodHandlerCall, MethodInArgAllocator, MethodInArgMaybeUninit, MethodInArgPtr, Result, Runtime, }; +/// Placeholder return sample for a method call result. +/// Wraps the return value and provides `Deref` access, +/// mirroring how `Sample` works for event data. +/// The real implementation will reference shared-memory backing (issue #782). +pub struct LolaMethodReturnSample { + value: T, +} + +impl Deref for LolaMethodReturnSample { + type Target = T; + fn deref(&self) -> &T { + &self.value + } +} + pub struct LolaMethodHandler { _phantom: core::marker::PhantomData<(Args, Return, R)>, } @@ -69,7 +85,7 @@ impl MethodCaller(&'a self, _args: Args) -> impl Future> + 'a { + fn invoke_with_copy<'a>(&'a self, _args: Args) -> impl Future>> + 'a { async move { todo!("Implement the logic to call the method with copied arguments") } } @@ -83,7 +99,7 @@ impl MethodCaller( &'a self, _ptrs: ::PtrTuple, - ) -> impl Future> + 'a { + ) -> impl Future>> + 'a { async move { todo!("Implement the logic to call the method with pre-allocated argument pointers") } diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs index 09cb28d94..9a5b5426b 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs @@ -17,8 +17,8 @@ use std::path::{Path, PathBuf}; use crate::{ LolaConsumerDiscovery, LolaConsumerInfo, LolaMethodCaller, LolaMethodHandler, - LolaMethodInArgAllocator, LolaProducerBuilder, LolaProviderInfo, LolaPublisher, - LolaSubscribableImpl, + LolaMethodInArgAllocator, LolaMethodReturnSample, LolaProducerBuilder, LolaProviderInfo, + LolaPublisher, LolaSubscribableImpl, }; use score_com_concept::{ Builder, CommData, FindServiceSpecifier, InstanceSpecifier, Interface, MethodArgs, Result, @@ -38,6 +38,7 @@ impl Runtime for LolaRuntimeImpl { type ProducerBuilder = LolaProducerBuilder; type Publisher = LolaPublisher; type MethodInArgAllocator = LolaMethodInArgAllocator; + type MethodReturnSample = LolaMethodReturnSample; type MethodCaller = LolaMethodCaller; type MethodHandler = LolaMethodHandler; type ProviderInfo = LolaProviderInfo; diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs index a4bc92d34..22595b05a 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs @@ -71,6 +71,7 @@ impl Runtime for MockRuntimeImpl { type ProducerBuilder = MockProducerBuilder; type Publisher = MockPublisher; type MethodInArgAllocator = MockMethodInArgAllocator; + type MethodReturnSample = MockMethodReturnSample; type MethodCaller = MockMethodCaller; type MethodHandler = MockMethodHandler; type ProviderInfo = MockProviderInfo; @@ -536,6 +537,20 @@ impl MethodHandler` access, +/// mirroring `LolaMethodReturnSample` in the LoLa runtime. +pub struct MockMethodReturnSample { + value: T, +} + +impl Deref for MockMethodReturnSample { + type Target = T; + fn deref(&self) -> &T { + &self.value + } +} + pub struct MockMethodCaller { _phantom: core::marker::PhantomData<(Args, Return, R)>, } @@ -552,7 +567,10 @@ impl MethodCaller(&'a self, _args: Args) -> impl Future> + 'a { + fn invoke_with_copy<'a>( + &'a self, + _args: Args, + ) -> impl Future>> + 'a { async move { todo!("Implement the logic to call the method with copied arguments") } } @@ -566,7 +584,7 @@ impl MethodCaller( &'a self, _ptrs: ::PtrTuple, - ) -> impl Future> + 'a { + ) -> impl Future>> + 'a { async move { todo!("Implement the logic to call the method with pre-allocated argument pointers") } diff --git a/score/mw/com/rust/score_com_concept/concept.rs b/score/mw/com/rust/score_com_concept/concept.rs index ed2e11c07..829668a10 100644 --- a/score/mw/com/rust/score_com_concept/concept.rs +++ b/score/mw/com/rust/score_com_concept/concept.rs @@ -103,6 +103,11 @@ pub trait Runtime { type MethodInArgAllocator: MethodInArgAllocator; + /// `MethodReturnSample` wraps the return value of a method call. + /// It provides `Deref` access to the return data, similar to`Sample` for + /// events, allowing the runtime to back the return value with shared memory without copying. + type MethodReturnSample: Deref; + /// `MethodCaller` types for calling methods on the proxy/consumer side type MethodCaller: MethodCaller; diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_macros.rs index 2091f9d80..2054ab6ac 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -689,7 +689,7 @@ macro_rules! interface_producer_mixed { } /// Entry-point wrapper generator. -/// Every generated wrapper returns `impl Future> + '_`. +/// Every generated wrapper returns `impl Future>> + '_`. /// /// # Generated call sites /// ```text @@ -702,7 +702,7 @@ macro_rules! _gen_method_wrapper { // 0 args - invoke_with_copy directly; no zero-copy path (nothing to allocate). // This is for kind of `get` methods that take no arguments and return a value. ($me_name:ident () -> $me_ret:ty) => { - pub fn $me_name<'a>(&'a self) -> impl core::future::Future> + 'a { + pub fn $me_name<'a>(&'a self) -> impl core::future::Future::MethodReturnSample<$me_ret>>> + 'a { score_com::MethodCaller::invoke_with_copy(&self.$me_name, ()) } }; @@ -766,7 +766,7 @@ macro_rules! _gen_method_wrapper_collect { /// All arities use this one arm - the function body is written once, not duplicated per arity. /// Called by `_gen_method_wrapper_collect!` after it has built the full triplet list. /// -/// The generated function returns `impl Future> + 'a` so callers +/// The generated function returns `impl Future>> + 'a` so callers /// can `.await` the method call, e.g. `consumer.method_name(arg0).await?`. #[doc(hidden)] #[macro_export] @@ -775,7 +775,7 @@ macro_rules! _gen_method_wrapper_body { pub fn $me_name<'a, $($g),+>( &'a self, $($p: $g),+ - ) -> impl core::future::Future> + 'a + ) -> impl core::future::Future::MethodReturnSample<$me_ret>>> + 'a where ($($g,)+): score_com::MethodCallInput<($($c,)+), $me_ret, R>, R::MethodCaller<($($c,)+), $me_ret>: diff --git a/score/mw/com/rust/score_com_concept/method_arities_macros.rs b/score/mw/com/rust/score_com_concept/method_arities_macros.rs index 68800e1ac..0d170abed 100644 --- a/score/mw/com/rust/score_com_concept/method_arities_macros.rs +++ b/score/mw/com/rust/score_com_concept/method_arities_macros.rs @@ -90,7 +90,7 @@ macro_rules! impl_all_arities { fn invoke<'a>( self, caller: &'a R::MethodCaller<($($T,)* $nextT,), Return>, - ) -> impl Future> + 'a + ) -> impl Future>> + 'a where R::MethodCaller<($($T,)* $nextT,), Return>: MethodCaller<($($T,)* $nextT,), Return, R> + 'a, diff --git a/score/mw/com/rust/score_com_concept/method_concept.rs b/score/mw/com/rust/score_com_concept/method_concept.rs index b84d5fca2..b49206273 100644 --- a/score/mw/com/rust/score_com_concept/method_concept.rs +++ b/score/mw/com/rust/score_com_concept/method_concept.rs @@ -130,9 +130,11 @@ pub trait MethodCaller /// # Arguments /// * `args` - The method arguments to pass to the method call. /// - /// Returns a future that resolves to a `Result` containing the method return value if any - /// otherwise unit or an error if the call failed. - fn invoke_with_copy<'a>(&'a self, args: Args) -> impl Future> + 'a; + /// Returns a future that resolves to a `Result` containing a `MethodReturnSample` + /// which provides `Deref` access to the return value, + /// analogous to `Sample` for events — allowing zero-copy access to the return data + /// when the runtime backs it with shared memory. + fn invoke_with_copy<'a>(&'a self, args: Args) -> impl Future>> + 'a; /// Allocate uninitialized method arguments for a zero-copy method call. /// @@ -158,12 +160,12 @@ pub trait MethodCaller /// # Arguments /// * `ptrs` - The pre-allocated method argument pointers to pass to the method call in a tuple. /// - /// Returns a future that resolves to a `Result` containing the method return value if any - /// otherwise unit or an error if the call failed. + /// Returns a future that resolves to a `Result` containing a `MethodReturnSample` + /// which provides `Deref` access to the return value. fn invoke_zero_copy<'a>( &'a self, ptrs: ::PtrTuple, - ) -> impl Future> + 'a; + ) -> impl Future>> + 'a; } /// This is the uninitialized type for a single method argument. It is used in the zero-copy method call path. @@ -259,12 +261,12 @@ pub trait MethodCallInput>`, + /// providing `Deref` access to the return value. fn invoke<'a>( self, caller: &'a R::MethodCaller, - ) -> impl Future> + 'a + ) -> impl Future>> + 'a where R::MethodCaller: MethodCaller + 'a; } @@ -281,7 +283,7 @@ where fn invoke<'a>( self, caller: &'a R::MethodCaller, - ) -> impl Future> + 'a + ) -> impl Future>> + 'a where R::MethodCaller: MethodCaller + 'a, { From 36d892caa32c8a70f230867bf2ef021c481a235e Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Tue, 28 Jul 2026 10:12:35 +0530 Subject: [PATCH 09/25] Rust::com Design documentation for Method APIs * Added design markdown file and diagrams --- .../com-api-example/src/method_consumer.rs | 18 +- .../com-api-example/src/method_producer.rs | 6 +- .../com/rust/design/design_document_method.md | 487 ++++++++++++++++++ score/mw/com/rust/design/method_overview.puml | 39 ++ score/mw/com/rust/design/method_overview.svg | 1 + .../com/rust/design/method_trait_diagram.puml | 123 +++++ .../com/rust/design/method_trait_diagram.svg | 1 + score/mw/com/rust/score_com.rs | 2 +- .../rust/score_com_concept/method_concept.rs | 7 +- 9 files changed, 668 insertions(+), 16 deletions(-) create mode 100644 score/mw/com/rust/design/design_document_method.md create mode 100644 score/mw/com/rust/design/method_overview.puml create mode 100644 score/mw/com/rust/design/method_overview.svg create mode 100644 score/mw/com/rust/design/method_trait_diagram.puml create mode 100644 score/mw/com/rust/design/method_trait_diagram.svg diff --git a/score/mw/com/example/com-api-example/src/method_consumer.rs b/score/mw/com/example/com-api-example/src/method_consumer.rs index 7fc1bffaf..f9e80eb20 100644 --- a/score/mw/com/example/com-api-example/src/method_consumer.rs +++ b/score/mw/com/example/com-api-example/src/method_consumer.rs @@ -15,8 +15,8 @@ // It shows how method can be called using copy and zero-copy arguments, // And async method call can be awaited to get the result. -// Notes: we are creating consumer instance specific for method here but this is just for demonstration perpose, -// for same consumer insatnce method / event/ field can be consume as per offer interface. +// Notes: we are creating consumer instance specific for method here but this is just for demonstration purpose, +// for same consumer instance method / event/ field can be consume as per offer interface. // This can not be used or called in main of example app, as runtime implementation is not available for method APIs. #![allow(unused)] @@ -87,14 +87,12 @@ async fn consumer_method_processing(consumer: VehicleMethodConsumer< // Get Method call which has no argument and return a value, which is also async. async fn method_get_call(consumer: VehicleMethodConsumer) { // Copy path: zero-argument method — empty parens, no empty-tuple needed. - futures::executor::block_on(async { - // it returns a `Result>` - // which is a wrapper around the return value of the method call. - match consumer.get_tire_pressure().await { - Ok(tire) => println!("Current tire pressure: {:?}", *tire), - Err(e) => eprintln!("Failed to call get_tire_pressure method: {:?}", e), - } - }); + // it returns a `Result>` + // which is a wrapper around the return value of the method call. + match consumer.get_tire_pressure().await { + Ok(tire) => println!("Current tire pressure: {:?}", *tire), + Err(e) => eprintln!("Failed to call get_tire_pressure method: {:?}", e), + } } // two arguments method. diff --git a/score/mw/com/example/com-api-example/src/method_producer.rs b/score/mw/com/example/com-api-example/src/method_producer.rs index 41ba44105..af4a9473d 100644 --- a/score/mw/com/example/com-api-example/src/method_producer.rs +++ b/score/mw/com/example/com-api-example/src/method_producer.rs @@ -13,8 +13,8 @@ // This file demonstrate the usage of producer method APIs, which are generated for the VehicleMethodsInterface. -// Notes: we are creating producer instance specific for method here but this is just for demonstration perpose, -// for same producer insatnce method / event/ field can be offered as per offer interface. +// Notes: we are creating producer instance specific for method here but this is just for demonstration purpose, +// for same producer instance method / event/ field can be offered as per offer interface. #![allow(unused)] @@ -32,7 +32,7 @@ type VehicleMethodOfferedProducer = // The method handlers are registered using the `register__handler` methods on the producer instance. // The handlers are registered before offering the producer instance, so that the consumer can call the methods on the producer instance. // If user call `producer.offer()` before registering the handlers, it will panic, as handlers are not registered yet. -// And if user cann the `producer.init()` but forget to register one of method halder then complier will give error, +// And if user call the `producer.init()` but forget to register one of method handler then compiler will give error, // as offer method using `init()` will require all method handlers to be registered before offering the producer instance. fn create_producer_method( runtime: &R, diff --git a/score/mw/com/rust/design/design_document_method.md b/score/mw/com/rust/design/design_document_method.md new file mode 100644 index 000000000..29f061f61 --- /dev/null +++ b/score/mw/com/rust/design/design_document_method.md @@ -0,0 +1,487 @@ + +# COM API-Method Design + +This document describes the design of the **method** APIs and usage of it. + +## Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Core Trait Design](#core-trait-design) + - [Runtime-Implemented Traits](#runtime-implemented-traits) + - [Allocation Traits](#allocation-traits) + - [Macro-Internal Supporting Traits](#macro-internal-supporting-traits) +- [Argument Arity Design](#argument-arity-design) +- [Copy vs Zero-Copy Call Paths](#copy-vs-zero-copy-call-paths) +- [Interface Macro Integration](#interface-macro-integration) +- [Type-State Validator](#type-state-validator) +- [Producer Side API Usage](#producer-side-api-usage) +- [Consumer Side API Usage](#consumer-side-api-usage) +- [TODOs and Improvements](#todos-and-improvements) + +--- + +## Overview + +Methods implement a request/response communication pattern: a consumer calls a method on a producer with typed arguments and asynchronously awaits a typed return value. + +- Method calls are always async and every generated wrapper returns `impl Future` and must be `.await`ed. +- Arguments can be passed by value (copy path) or via pre-allocated pointers (zero-copy path) using the same call site and the compiler selects the correct dispatch based on argument type. +- Return values are wrapped in `R::MethodReturnSample`, which provides `Deref` access, allowing the runtime to back the return with shared memory without an extra copy. +- Handler registration on the producer side is enforced at compile time via the type-state validator, `offer()` is only callable after all method handlers are registered. Bypassing the validator and calling `offer()` directly will panic at runtime. + +Methods are defined as part of an interface via the `interface!` macro alongside events and fields: + +```rust +interface!( + interface VehicleMethods { + Id = "VehicleMethodsInterface", + update_tire_pressure(Tire) -> (), + update_front_tires_pressure(Tire, Tire) -> (), + get_tire_pressure() -> Tire, + } +); +``` + +The macro uses `fn`-like syntax: `method_name(ArgType0, ArgType1, ...) -> ReturnType`. For void return, `-> ()` is required. + +--- + +## Architecture + +The method feature follows the same layered architecture as the rest of the COM API. + +![Method Overview](method_overview.svg) + +> Source: [method_overview](method_overview.svg) + +| Layer | Role | +|-------|------| +| **Application** | User code calls `consumer.method(args).await` and registers `producer.init().register_handler(fn).offer()` | +| **Abstraction** | Platform-independent method traits in `score_com_concept`, `interface!` macro generates typed wrappers, `type_state_validator` enforces compile-time correctness | +| **Runtime** | Concrete `LolaMethodCaller` / `LolaMethodHandler` in `com-api-runtime-lola` that translate trait calls to FFI operations | +| **FFI** | Rust–C++ bindings bridging method dispatch and handler registration to the underlying middleware | + +--- + +## Core Trait Design + +The full trait diagram is shown below. Source: [method_trait_diagram](method_trait_diagram.svg). + +![Method Trait Diagram](method_trait_diagram.svg) + +Traits split into three groups based on who implements them. + +### Runtime-Implemented Traits + +These traits must be implemented by every runtime (e.g. `com-api-runtime-lola`). + +#### `MethodHandler` + +Producer-side handler registration. The runtime is responsible for setting up the dispatch mechanism (e.g. thread pool, async executor) that receives incoming calls and routes them to the registered handler. + +```rust +pub trait MethodHandler { + fn new(method_name: &str, instance_info: R::ProviderInfo) -> Result + where + Self: Sized; + + fn register_handler(&self, handler: F) + where + F: MethodHandlerCall; +} +``` + +The `register_handler` call accepts any value that satisfies `MethodHandlerCall` and this is automatically satisfied by closures and function pointers with the matching signature. + +Note: The runtime may dispatch incoming calls concurrently to the same handler. Handlers must synchronize any access to shared mutable state internally. + +#### `MethodCaller` + +Consumer-side method invocation. Provides both the copy path and the zero-copy path, plus argument allocation. + +```rust +pub trait MethodCaller { + fn new(method_name: &str, instance_info: R::ConsumerInfo) -> Result + where + Self: Sized; + + fn invoke_with_copy<'a>(&'a self, args: Args) + -> impl Future>> + 'a; + + fn allocate(&self) + -> Result<>::UninitTuple> + where + Args: MethodArgsAllocate; + + fn invoke_zero_copy<'a>(&'a self, ptrs: ::PtrTuple) + -> impl Future>> + 'a; +} +``` + +`invoke_with_copy` and `invoke_zero_copy` are not intended to be called by application code directly. The `interface!` macro generates a single wrapper per method on the consumer that accepts both forms transparently via `MethodCallInput` (see [Copy vs Zero-Copy Call Paths](#copy-vs-zero-copy-call-paths)). + +### Allocation Traits + +These traits form the zero-copy argument allocation pipeline. + +#### `MethodReturnSample` + +A runtime-defined wrapper for the method return value. It implements `Deref`, giving the caller read access to the returned data without an additional copy, this is similar to `Sample` for event data on the consumer side. + +The `Runtime` trait declares it as an associated type because interface macro needs to access this and also it is runtime specific. + +```rust +type MethodReturnSample: Deref; +``` + +Concrete implementations: +- `LolaMethodReturnSample` - in `com-api-runtime-lola` +- `MockMethodReturnSample` - in `com-api-runtime-mock` + +The consumer dereferences the sample to access the return value: + +```rust +let sample = consumer.get_tire_pressure().await?; +let pressure: &Tire = &*sample; // Deref +``` + +Once the LoLa FFI implementation is complete (issue #https://github.com/eclipse-score/communication/issues/782), `LolaMethodReturnSample` will reference a shared-memory slot rather than an owned copy. + +#### `MethodInArgAllocator` + +Runtime-specific allocator for method input arguments. An instance lives on the `MethodCaller` and hands out uninitialised slots. + +```rust +pub trait MethodInArgAllocator { + type MethodInArgMaybeUninit: MethodInArgMaybeUninit; + + fn allocate(&self) -> Self::MethodInArgMaybeUninit; +} +``` + +#### `MethodInArgMaybeUninit` + +A single uninitialised argument slot. The caller writes a value into it, obtaining an initialised `MethodInArgPtr` that can be passed to the method call. + +```rust +pub trait MethodInArgMaybeUninit { + fn write(self, val: T) -> MethodInArgPtr; + + /// # Safety + /// The caller must ensure the memory has been properly initialized. + unsafe fn assume_init(self) -> MethodInArgPtr; +} +``` + +#### `MethodInArgPtr` + +A pointer to a fully-initialised, pre-allocated method argument. It is used in the zero-copy call path instead of passing `T` by value. + +```rust +pub struct MethodInArgPtr { + pub _phantom: core::marker::PhantomData, +} +``` + +> **Note**: `MethodInArgPtr` is currently a placeholder. Real shared-memory layout support is tracked in [issue #781](https://github.com/eclipse-score/communication/issues/781). + +### Macro-Internal Supporting Traits + +These traits are not implemented by runtimes. Blanket implementations are provided in `score_com_concept` for all supported arities (currently 0-8) via the `impl_all_arities!` macro in `method_arities_macros.rs`. +**Note**: `impl_all_arities!` is an internal implementation detail invoked automatically by the framework. User should not invoke or reference this macro directly. + +#### `MethodArgs` + +Marker trait for method argument tuples. Carries `PtrTuple`- the matching tuple of `MethodInArgPtr` values used in the zero-copy path. + +```rust +pub trait MethodArgs: CommData { + type PtrTuple; +} +// e.g. (Tire, Tire)::PtrTuple = (MethodInArgPtr, MethodInArgPtr) +``` + +#### `MethodArgsAllocate` + +Maps an `Args` tuple to the matching uninitialised argument tuple for a specific allocator `A`. + +```rust +pub trait MethodArgsAllocate: MethodArgs { + type UninitTuple; + fn alloc_uninit(allocator: &A) -> Self::UninitTuple; +} +// e.g. (Tire, Tire)::UninitTuple = (A::MethodInArgMaybeUninit, A::MethodInArgMaybeUninit) +``` + +#### `MethodCallInput` + +Unified input type for the `interface!`- generated consumer method wrapper. Implemented for both `Args` (copy path) and `Args::PtrTuple` (zero-copy path). The compiler selects the correct impl from the type passed at the call site - no runtime branching. + +```rust +pub trait MethodCallInput { + fn invoke<'a>( + self, + caller: &'a R::MethodCaller, + ) -> impl Future>> + 'a; +} +``` + +This is what allows a single generated method on the consumer to accept both calling conventions: + +```rust +consumer.update_tire_pressure(tire) // copy path-Args impl +consumer.update_tire_pressure(tire_ptr) // zero-copy path-PtrTuple impl +``` + +#### `MethodHandlerCall` + +Callable abstraction for handler functions. Any `Fn` closure or function pointer with the matching signature automatically satisfies this trait through blanket impls for all arities. + +```rust +pub trait MethodHandlerCall: Send + Sync + 'static { + fn call(&self, args: Args) -> Return; +} +``` + +--- + +## Argument Arity Design + +Rust does not support variadic functions. Methods need to accept 0 - 8 typed arguments. Rather than generating separate trait implementations for each arity, the design uses argument tuples and a single `impl_all_arities!` macro in `method_arities_macros.rs`. + +The macro generates blanket implementations of the following traits for each arity (0-8): + +| Trait | Why blanket | +|-------|-------------| +| `MethodArgs` | `PtrTuple` construction per arity | +| `MethodArgsAllocate` | `alloc_uninit` loops per arity | +| `MethodCallInput` | Zero-copy path dispatches per arity | +| `MethodHandlerCall` | Handler `call()` unpacks tuple per arity | +| `Reloc` | Arg tuple is relocatable if all elements are | +| `CommData` | Arg tuple is `Communication data` if all elements are | + +The result: adding a new runtime requires only implementing `MethodCaller` and `MethodHandler`. All arity-specific glue is already provided. + +--- + +## Copy vs Zero-Copy Call Paths + +Both paths invoke the same generated method wrapper on the consumer. The compiler chooses the implementation based on the argument type. + +**Copy path**-pass arguments by value: + +```rust +// Args = (Tire,) - MethodCallInput impl for (Tire,) - invoke_with_copy +let tire = Tire { pressure: 30.0 }; +consumer.update_tire_pressure(tire).await?; +``` + +**Zero-copy path**-allocate, write, then call with pre-allocated pointers: + +```rust +// Allocate uninit slots from the runtime's MethodInArgAllocator +let (uninit,) = consumer.update_tire_pressure.allocate()?; +// Write the value into the slot, get back an initialised pointer +let tire_ptr = uninit.write(Tire { pressure: 35.0 }); +// PtrTuple = (MethodInArgPtr,) - MethodCallInput impl for PtrTuple - invoke_zero_copy +consumer.update_tire_pressure(tire_ptr).await?; +``` + +The zero-copy path avoids copying argument data when the runtime backs arguments with shared memory. When the `MethodInArgPtr` layout is fully implemented (issue https://github.com/eclipse-score/communication/issues/781), the write step will place data directly in the shared-memory slot used by the FFI call. + +--- + +## Interface Macro Integration + +The `interface!` macro in `interface_macros.rs` accepts methods using fn-like syntax: + +```rust +interface!( + interface VehicleMethods { + Id = "VehicleMethodsInterface", + update_tire_pressure(Tire) -> (), + get_tire_pressure() -> Tire, + update_front_tires_pressure(Tire, Tire) -> (), + } +); +``` + +For each method, the macro generates: + +**On `VehicleMethodsConsumer`**-a callable wrapper field. The field is a struct that: +- Implements `AsyncFn`semantics so `consumer.update_tire_pressure(arg).await` works +- Exposes `.allocate()` for the zero-copy path +- Holds a reference to the runtime's `R::MethodCaller<(Tire,), ()>` instance +- Calls through `MethodCallInput::invoke()` to dispatch to the correct `MethodCaller` method + +**On `VehicleMethodsValidator`** (returned by `producer.init()`)- a `register_update_tire_pressure_handler(fn)` method that: +- Accepts any `F: MethodHandlerCall<(Tire,), ()>`-i.e. any matching closure or fn pointer +- Calls `MethodHandler::register_handler(handler)` on the runtime's handler instance +- Advances the type-state (see [Type-State Validator](#type-state-validator)) + +--- + +## Type-State Validator + +The `type_state_validator` proc-macro in `score_com_macros` generates a compile-time state machine on the producer initialisation path. Each method handler registration is tracked as a generic type parameter that transitions from `HandlerNotSet` to `HandlerSet`. + +`offer()` is only available once all parameters are in the `HandlerSet` state. Calling `offer()` before registering all handlers is a compile error. + +```rust +// Compile error: offer() not available until all three handlers are registered +producer.init() + .register_update_tire_pressure_handler(process_tire) + // missing: register_get_tire_pressure_handler + // missing: register_update_front_tires_pressure_handler + .offer() // compile error +``` + +```rust +// Correct: all handlers registered +producer + .init() + .register_update_tire_pressure_handler(process_tire) + .register_get_tire_pressure_handler(|| Tire { pressure: 32.0 }) + .register_update_front_tires_pressure_handler(|t1, t2| { /* ... */ }) + .offer()?; +``` + +Note: For event-only interfaces, `producer.offer()` can be called directly. For methods and fields, `offer()` must be called via the type-state validator path using `producer.init()` and calling `offer()` directly will panic. + +--- + +## Producer Side API Usage + +The following example is drawn from [`com-api-example/src/method_producer.rs`](../../example/com-api-example/src/method_producer.rs). + +```rust +use score_com::{Builder, InstanceSpecifier, Interface, Producer, Runtime}; +use com_api_gen::{Tire, VehicleMethodsInterface}; + +fn create_producer( + runtime: &R, + service_id: InstanceSpecifier, +) -> <::Producer as Producer>::OfferedProducer { + let producer = runtime + .producer_builder::(service_id) + .build() + .expect("Failed to build producer"); + + producer + .init() + // Register with a named function pointer + .register_update_tire_pressure_handler(process_tire) + // Register with a closure-zero-argument method returning a value + .register_get_tire_pressure_handler(|| Tire { pressure: 32.0 }) + // Register with a closure-two-argument method + .register_update_front_tires_pressure_handler(|t1: Tire, t2: Tire| { + println!("Front tires: {:?}, {:?}", t1, t2); + }) + .offer() + .expect("Failed to offer producer") +} + +fn process_tire(tire: Tire) { + println!("Tire pressure: {:?}", tire); +} +``` + +Key points: + +- `producer.init()` returns the generated `Validator` type; each `register_*` call advances the type-state. +- Handlers are registered before `offer()`, which is the only way to make the service discoverable. +- Handlers may be closures or function pointers-any type satisfying `MethodHandlerCall`. +- The runtime may call handlers concurrently. Handlers must synchronise any shared mutable state internally. + +--- + +## Consumer Side API Usage + +The following examples are drawn from [`com-api-example/src/method_consumer.rs`](../../example/com-api-example/src/method_consumer.rs). + +All method calls return `impl Future>>` and must be `.await`ed. +The returned sample implements `Deref`. + +### Single-argument-copy path + +```rust +let tire = Tire { pressure: 30.0 }; +match consumer.update_tire_pressure(tire).await { + Ok(_) => println!("Method called successfully"), + Err(e) => eprintln!("Error: {:?}", e), +} +``` + +### Single-argument-zero-copy path + +```rust +let (uninit,) = consumer + .update_tire_pressure + .allocate() + .expect("Allocation failed"); + +let tire_ptr = uninit.write(Tire { pressure: 35.0 }); + +match consumer.update_tire_pressure(tire_ptr).await { + Ok(_) => println!("Zero-copy method called successfully"), + Err(e) => eprintln!("Error: {:?}", e), +} +``` + +### No-argument method returning a value + +```rust +match consumer.get_tire_pressure().await { + Ok(sample) => println!("Current pressure: {:?}", *sample), // Deref to access Tire + Err(e) => eprintln!("Error: {:?}", e), +} +``` + +### Two-argument-copy path and zero-copy path + +```rust +// Copy path +let (t1, t2) = (Tire { pressure: 31.0 }, Tire { pressure: 32.0 }); +consumer.update_front_tires_pressure(t1, t2).await?; + +// Zero-copy path +let (uninit1, uninit2) = consumer + .update_front_tires_pressure + .allocate() + .expect("Allocation failed"); + +let ptr1 = uninit1.write(Tire { pressure: 36.0 }); +let ptr2 = uninit2.write(Tire { pressure: 37.0 }); +consumer.update_front_tires_pressure(ptr1, ptr2).await?; +``` +--- + +## TODOs and Improvements + +### Issue #782-LoLa runtime FFI implementation + +All `LolaMethodCaller` and `LolaMethodHandler` methods in `com-api-runtime-lola/method.rs` are currently `todo!()` placeholders. The FFI bindings to the underlying C++ middleware for method invocation and handler registration are not yet implemented. This blocks all end-to-end method call tests. + +See: + +### Issue #781-Replace `MethodInArgPtr` with a real shared-memory pointer + +`MethodInArgPtr` is currently a `PhantomData` placeholder with no memory backing. Once the runtime implements zero-copy method arguments over shared memory, this type needs to carry a real pointer or reference to the allocated slot, with appropriate lifetime and safety constraints-analogous to `SampleMut` for events. + +See: + +### If required add a blocking `.wait()` convenience for sync callers + +Method calls return futures. Callers without an async executor must bring their own (e.g. `futures::executor::block_on`). A convenience wrapper-similar to what `block_on` provides-should be added so that synchronous application code can call methods without pulling in an async runtime. diff --git a/score/mw/com/rust/design/method_overview.puml b/score/mw/com/rust/design/method_overview.puml new file mode 100644 index 000000000..5084bf10b --- /dev/null +++ b/score/mw/com/rust/design/method_overview.puml @@ -0,0 +1,39 @@ +@startuml + +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' +' +' SPDX-License-Identifier: Apache-2.0 + +skinparam linetype ortho +skinparam backgroundColor #FAFAFA +skinparam defaultFontSize 12 +skinparam ArrowColor #333333 +skinparam packageStyle rectangle +skinparam defaultTextAlignment center + +title Method — High-Level Block Overview + +[User Application\n(Producer / Consumer)] as APP + +[score_com_concept\n(MethodCaller, MethodHandler,\nMethodArgs, MethodCallInput,\nMethodInArgAllocator,\nMethodReturnSample)] as CONCEPT + +[interface!() macro\n+ type_state_validator\n(Generated consumer wrappers\n& producer Validator)] as MACRO + +[com-api-runtime-lola\n(LolaMethodCaller,\nLolaMethodHandler,\nLolaMethodInArgAllocator,\nLolaMethodReturnSample)] as RUNTIME + +[FFI\n(Rust-C++ method\ndispatch bindings)] as FFI + +APP --> MACRO : uses generated\nconsumer / producer API +MACRO --> CONCEPT : generated code\nuses concept traits +RUNTIME ..|> CONCEPT : implements +MACRO --> RUNTIME : dispatches at runtime +RUNTIME --> FFI : bridges via + +@enduml diff --git a/score/mw/com/rust/design/method_overview.svg b/score/mw/com/rust/design/method_overview.svg new file mode 100644 index 000000000..d6eb6a7ea --- /dev/null +++ b/score/mw/com/rust/design/method_overview.svg @@ -0,0 +1 @@ +Method — High-Level Block OverviewUser Application(Producer / Consumer)score_com_concept(MethodCaller, MethodHandler,MethodArgs, MethodCallInput,MethodInArgAllocator,MethodReturnSample)interface!() macro+ type_state_validator(Generated consumer wrappers& producer Validator)com-api-runtime-lola(LolaMethodCaller,LolaMethodHandler,LolaMethodInArgAllocator,LolaMethodReturnSample)FFI(Rust-C++ methoddispatch bindings)uses generatedconsumer / producer APIgenerated codeuses concept traitsimplementsdispatches at runtimebridges via \ No newline at end of file diff --git a/score/mw/com/rust/design/method_trait_diagram.puml b/score/mw/com/rust/design/method_trait_diagram.puml new file mode 100644 index 000000000..d730a6d32 --- /dev/null +++ b/score/mw/com/rust/design/method_trait_diagram.puml @@ -0,0 +1,123 @@ +@startuml + +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' +' +' SPDX-License-Identifier: Apache-2.0 + + +skinparam backgroundColor #FAFAFA +skinparam defaultFontSize 12 +skinparam ArrowColor #333333 +skinparam packageStyle rectangle +skinparam defaultTextAlignment center + +interface Runtime { + type MethodCaller + type MethodHandler + type MethodInArgAllocator + type MethodReturnSample + --- + + find_service() + + producer_builder() +} + +interface MethodHandler { + --- + + new(method_name, instance_info) -> Result + + register_handler(handler: impl MethodHandlerCall) +} + +interface MethodCaller { + --- + + new(method_name, instance_info) -> Result + + invoke_with_copy(args: Args) -> impl Future>> + + allocate() -> Result + + invoke_zero_copy(ptrs: Args::PtrTuple) -> impl Future>> +} + +interface MethodArgs { + type PtrTuple + --- + ' Blanket impls for arities 0-8 via impl_all_arities!() + ' PtrTuple = (MethodInArgPtr, ..., MethodInArgPtr) +} + +interface "MethodArgsAllocate" as MethodArgsAllocate { + type UninitTuple + --- + ' Blanket impls for arities 0-8 via impl_all_arities!() + + alloc_uninit(allocator: &A) -> UninitTuple +} + +interface "MethodCallInput" as MethodCallInput { + --- + + invoke(caller: &R::MethodCaller) -> impl Future>> + --- + ' Copy path: impl for Args itself (dispatches invoke_with_copy) + ' Zero-copy path: impl for Args::PtrTuple (dispatches invoke_zero_copy) + ' Both impls are blanket - no per-arity code in runtimes +} + +interface "MethodHandlerCall" as MethodHandlerCall { + --- + + call(args: Args) -> Return + --- + ' Blanket impl for Fn(...) -> Return + Send + Sync + 'static + ' Covers all arities 0-8 via impl_all_arities!() +} + +interface "MethodInArgAllocator" as MethodInArgAllocator { + type MethodInArgMaybeUninit + --- + + allocate() -> MethodInArgMaybeUninit +} + +interface "MethodInArgMaybeUninit" as MethodInArgMaybeUninit { + --- + + write(val: T) -> MethodInArgPtr + + assume_init() -> MethodInArgPtr +} + +class "MethodReturnSample" as MethodReturnSample { + --- + ' Wraps method return value, provides Deref access + ' Allows zero-copy return from shared memory (like Sample for events) + ' Concrete types: LolaMethodReturnSample, MockMethodReturnSample +} + +class "MethodInArgPtr" as MethodInArgPtr { + --- + ' Placeholder for pre-allocated shared-memory argument pointer + ' Real layout implementation pending (issue #781) +} + +Runtime --> MethodCaller : defines as\nassociated type +Runtime --> MethodHandler : defines as\nassociated type +Runtime --> MethodInArgAllocator : defines as\nassociated type +Runtime --> MethodReturnSample : defines as\nassociated type + +MethodCaller --> MethodArgs : requires Args bound +MethodCaller --> MethodArgsAllocate : uses for allocate() +MethodCaller --> MethodInArgAllocator : via Runtime associated type +MethodCaller --> MethodReturnSample : invoke returns + +MethodHandler --> MethodHandlerCall : accepts in\nregister_handler() + +MethodArgsAllocate --|> MethodArgs : extends + +MethodInArgAllocator --> MethodInArgMaybeUninit : allocate() produces + +MethodInArgMaybeUninit --> MethodInArgPtr : write() returns + +MethodArgs --> MethodInArgPtr : PtrTuple\ncomposed of + +MethodCallInput --> MethodCaller : invoke() dispatches to\ninvoke_with_copy or\ninvoke_zero_copy + +@enduml diff --git a/score/mw/com/rust/design/method_trait_diagram.svg b/score/mw/com/rust/design/method_trait_diagram.svg new file mode 100644 index 000000000..8f988b4ae --- /dev/null +++ b/score/mw/com/rust/design/method_trait_diagram.svg @@ -0,0 +1 @@ +Runtimetype MethodCaller<Args, Return>type MethodHandler<Args, Return>type MethodInArgAllocatortype MethodReturnSample<T: CommData>find_service()producer_builder()MethodHandlerArgs: MethodArgs, Return: CommData, R: Runtimenew(method_name, instance_info) -> Result<Self>register_handler(handler: impl MethodHandlerCall<Args, Return>)MethodCallerArgs: MethodArgs, Return: CommData, R: Runtimenew(method_name, instance_info) -> Result<Self>invoke_with_copy(args: Args) -> impl Future<Output = Result<R::MethodReturnSample<Return>>>allocate() -> Result<Args::UninitTuple>invoke_zero_copy(ptrs: Args::PtrTuple) -> impl Future<Output = Result<R::MethodReturnSample<Return>>>MethodArgstype PtrTupleMethodArgsAllocateA: MethodInArgAllocatortype UninitTuplealloc_uninit(allocator: &A) -> UninitTupleMethodCallInputArgs, Return, Rinvoke(caller: &R::MethodCaller<Args, Return>) -> impl Future<Output = Result<R::MethodReturnSample<Return>>>MethodHandlerCallArgs, Returncall(args: Args) -> ReturnMethodInArgAllocatortype MethodInArgMaybeUninit<T: CommData>allocate<T: CommData>() -> MethodInArgMaybeUninit<T>MethodInArgMaybeUninitTwrite(val: T) -> MethodInArgPtr<T>assume_init() -> MethodInArgPtr<T>MethodReturnSampleTMethodInArgPtrTdefines asassociated typedefines asassociated typedefines asassociated typedefines asassociated typerequires Args bounduses for allocate()via Runtime associated typeinvoke returnsaccepts inregister_handler()extendsallocate() produceswrite() returnsPtrTuplecomposed ofinvoke() dispatches toinvoke_with_copy orinvoke_zero_copy \ No newline at end of file diff --git a/score/mw/com/rust/score_com.rs b/score/mw/com/rust/score_com.rs index 83f213c02..4cfd6b429 100644 --- a/score/mw/com/rust/score_com.rs +++ b/score/mw/com/rust/score_com.rs @@ -134,7 +134,7 @@ //! Note: Event APIs are fully supported with the Lola runtime. //! Method API traits exported from this crate reflect the intended design but are not yet backed //! by a Lola runtime implementation in Rust side, they serve as a demonstration of the planned interface. -//! Method APIs developmement is tracked in +//! Method APIs development is tracked in //! https://github.com/eclipse-score/communication/issues/782 pub use com_api_runtime_lola::LolaRuntimeImpl; diff --git a/score/mw/com/rust/score_com_concept/method_concept.rs b/score/mw/com/rust/score_com_concept/method_concept.rs index b49206273..596c501af 100644 --- a/score/mw/com/rust/score_com_concept/method_concept.rs +++ b/score/mw/com/rust/score_com_concept/method_concept.rs @@ -134,7 +134,10 @@ pub trait MethodCaller /// which provides `Deref` access to the return value, /// analogous to `Sample` for events — allowing zero-copy access to the return data /// when the runtime backs it with shared memory. - fn invoke_with_copy<'a>(&'a self, args: Args) -> impl Future>> + 'a; + fn invoke_with_copy<'a>( + &'a self, + args: Args, + ) -> impl Future>> + 'a; /// Allocate uninitialized method arguments for a zero-copy method call. /// @@ -209,7 +212,7 @@ pub trait MethodInArgAllocator { /// For example, `(Tire, Tire)::PtrTuple = (MethodInArgPtr, MethodInArgPtr)`. /// /// Runtimes do not implement this trait. -/// Blanket impls for all supported arities (0–2 arguments) are provided in this crate. +/// Blanket impls for all supported arities (0–8 arguments) are provided in this crate. pub trait MethodArgs: CommData { type PtrTuple; } From 394cc7aae0c1ecb90b2a45d0d64f8403d9480579 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Tue, 28 Jul 2026 14:35:19 +0530 Subject: [PATCH 10/25] Rust::com MethodReturnSample and MethodInArgPtr trait added * Added MethodReturnSample for returning value at consumer side * MethodInArgPtr trait added for argument type --- .../mw/com/example/com-api-example/src/lib.rs | 5 +- .../com-api-example/src/method_consumer.rs | 5 +- .../com-api-example/src/method_producer.rs | 9 +- .../com-api/com-api-runtime-lola/method.rs | 33 ++++- .../com-api/com-api-runtime-mock/runtime.rs | 40 ++++-- .../com/rust/design/design_document_method.md | 93 +++++++----- .../com/rust/design/method_trait_diagram.puml | 63 ++++++--- .../com/rust/design/method_trait_diagram.svg | 2 +- score/mw/com/rust/score_com.rs | 11 +- .../mw/com/rust/score_com_concept/concept.rs | 11 +- .../score_com_concept/interface_macros.rs | 132 ++---------------- .../method_arities_macros.rs | 42 +++--- .../rust/score_com_concept/method_concept.rs | 96 +++++++++---- score/mw/com/rust/score_com_concept/reloc.rs | 2 + 14 files changed, 297 insertions(+), 247 deletions(-) diff --git a/score/mw/com/example/com-api-example/src/lib.rs b/score/mw/com/example/com-api-example/src/lib.rs index 293780e53..aff5cb729 100644 --- a/score/mw/com/example/com-api-example/src/lib.rs +++ b/score/mw/com/example/com-api-example/src/lib.rs @@ -12,8 +12,9 @@ ********************************************************************************/ pub mod consumer; -pub mod method_consumer; -pub mod method_producer; +// Method modules are just for demonstration purpose, as runtime implementation is not available for method APIs. +mod method_consumer; +mod method_producer; pub mod producer; pub use consumer::VehicleMonitorConsumer; pub use producer::VehicleMonitorProducer; diff --git a/score/mw/com/example/com-api-example/src/method_consumer.rs b/score/mw/com/example/com-api-example/src/method_consumer.rs index f9e80eb20..81c7c5cdc 100644 --- a/score/mw/com/example/com-api-example/src/method_consumer.rs +++ b/score/mw/com/example/com-api-example/src/method_consumer.rs @@ -19,6 +19,8 @@ // for same consumer instance method / event/ field can be consume as per offer interface. // This can not be used or called in main of example app, as runtime implementation is not available for method APIs. +// All the functions and types in this file are just for demonstration purpose, +// as this are not part of any callable because of that unused warning is suppressed for this file. #![allow(unused)] use score_com::{ @@ -67,7 +69,8 @@ async fn consumer_method_processing(consumer: VehicleMethodConsumer< Ok(_) => println!("Successfully called update_tire_pressure method"), Err(e) => eprintln!("Failed to call update_tire_pressure method: {:?}", e), } - + // Allocate return the tuple of uninitialized method argument slots, + // We need to store in tuple format, or user need to access using uninit1.0.write(...) let (uninit1,) = consumer .update_tire_pressure .allocate() diff --git a/score/mw/com/example/com-api-example/src/method_producer.rs b/score/mw/com/example/com-api-example/src/method_producer.rs index af4a9473d..4c612d313 100644 --- a/score/mw/com/example/com-api-example/src/method_producer.rs +++ b/score/mw/com/example/com-api-example/src/method_producer.rs @@ -16,6 +16,8 @@ // Notes: we are creating producer instance specific for method here but this is just for demonstration purpose, // for same producer instance method / event/ field can be offered as per offer interface. +// All the functions and types in this file are just for demonstration purpose, +// as this are not part of any callable because of that unused warning is suppressed for this file. #![allow(unused)] use score_com::{Builder, InstanceSpecifier, Interface, Producer, Runtime}; @@ -32,8 +34,11 @@ type VehicleMethodOfferedProducer = // The method handlers are registered using the `register__handler` methods on the producer instance. // The handlers are registered before offering the producer instance, so that the consumer can call the methods on the producer instance. // If user call `producer.offer()` before registering the handlers, it will panic, as handlers are not registered yet. -// And if user call the `producer.init()` but forget to register one of method handler then compiler will give error, -// as offer method using `init()` will require all method handlers to be registered before offering the producer instance. +// User must call the `producer.init()` and register all method handlers, +// and if they forget to register one of the method handlers then the compiler will give an error, +// as the offer method will require all method handlers to be registered before offering the producer instance. +// Here assumption of use is user must call`init()` and register all method handlers , +// direct call to `offer()` API will panic. fn create_producer_method( runtime: &R, service_id: InstanceSpecifier, diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs index ef17e0db5..ad1cbf1e1 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs @@ -20,8 +20,9 @@ use core::future::Future; use core::ops::Deref; use score_com_concept::{ - CommData, MethodArgs, MethodArgsAllocate, MethodCaller, MethodHandler, MethodHandlerCall, - MethodInArgAllocator, MethodInArgMaybeUninit, MethodInArgPtr, Result, Runtime, + CommData, MethodArgs, MethodArgsAllocate, MethodArgsPtrTuple, MethodCaller, MethodHandler, + MethodHandlerCall, MethodInArgAllocator, MethodInArgMaybeUninit, MethodInArgPtr, + MethodReturnSample, Result, Runtime, ZeroCopyArgs, }; /// Placeholder return sample for a method call result. @@ -39,6 +40,8 @@ impl Deref for LolaMethodReturnSample { } } +impl MethodReturnSample for LolaMethodReturnSample {} + pub struct LolaMethodHandler { _phantom: core::marker::PhantomData<(Args, Return, R)>, } @@ -85,7 +88,10 @@ impl MethodCaller(&'a self, _args: Args) -> impl Future>> + 'a { + fn invoke_with_copy<'a>( + &'a self, + _args: Args, + ) -> impl Future>> + 'a { async move { todo!("Implement the logic to call the method with copied arguments") } } @@ -98,8 +104,11 @@ impl MethodCaller( &'a self, - _ptrs: ::PtrTuple, - ) -> impl Future>> + 'a { + _ptrs: >::PtrTuple, + ) -> impl Future>> + 'a + where + Args: MethodArgsPtrTuple, + { async move { todo!("Implement the logic to call the method with pre-allocated argument pointers") } @@ -111,12 +120,21 @@ pub struct LolaMethodInArgMaybeUninit { _phantom: core::marker::PhantomData, } +/// Runtime-specific concrete type for a fully-initialised Lola method argument pointer. +pub struct LolaMethodInArgPtr { + _phantom: core::marker::PhantomData, +} + +impl MethodInArgPtr for LolaMethodInArgPtr {} + impl MethodInArgMaybeUninit for LolaMethodInArgMaybeUninit { - fn write(self, _val: T) -> MethodInArgPtr { + type Ptr = LolaMethodInArgPtr; + + fn write(self, _val: T) -> ZeroCopyArgs> { todo!("Implement write into Lola shared-memory slot"); } - unsafe fn assume_init(self) -> MethodInArgPtr { + unsafe fn assume_init(self) -> ZeroCopyArgs> { todo!("Implement assume_init for Lola shared-memory slot"); } } @@ -125,6 +143,7 @@ impl MethodInArgMaybeUninit for LolaMethodInArgMaybeUninit { pub struct LolaMethodInArgAllocator; impl MethodInArgAllocator for LolaMethodInArgAllocator { + type MethodInArgPtr = LolaMethodInArgPtr; type MethodInArgMaybeUninit = LolaMethodInArgMaybeUninit; fn allocate(&self) -> LolaMethodInArgMaybeUninit { todo!("Implement allocation from the Lola shared-memory region via &self context"); diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs index 22595b05a..3a3e49dd9 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs @@ -37,10 +37,11 @@ use std::path::Path; use score_com_concept::{ Builder, CommData, Consumer, ConsumerBuilder, ConsumerDescriptor, FindServiceSpecifier, - InstanceSpecifier, Interface, MethodArgs, MethodArgsAllocate, MethodCaller, MethodHandler, - MethodHandlerCall, MethodInArgAllocator, MethodInArgMaybeUninit, MethodInArgPtr, Producer, - ProducerBuilder, ProviderInfo, Publisher, Result, Runtime, RuntimeBuilder, Sample, - SampleContainer, SampleMaybeUninit, SampleMut, ServiceDiscovery, Subscriber, Subscription, + InstanceSpecifier, Interface, MethodArgs, MethodArgsAllocate, MethodArgsPtrTuple, MethodCaller, + MethodHandler, MethodHandlerCall, MethodInArgAllocator, MethodInArgMaybeUninit, MethodInArgPtr, + MethodReturnSample, Producer, ProducerBuilder, ProviderInfo, Publisher, Result, Runtime, + RuntimeBuilder, Sample, SampleContainer, SampleMaybeUninit, SampleMut, ServiceDiscovery, + Subscriber, Subscription, ZeroCopyArgs, }; pub struct MockRuntimeImpl {} @@ -551,6 +552,8 @@ impl Deref for MockMethodReturnSample { } } +impl MethodReturnSample for MockMethodReturnSample {} + pub struct MockMethodCaller { _phantom: core::marker::PhantomData<(Args, Return, R)>, } @@ -583,8 +586,11 @@ impl MethodCaller( &'a self, - _ptrs: ::PtrTuple, - ) -> impl Future>> + 'a { + _ptrs: >::PtrTuple, + ) -> impl Future>> + 'a + where + Args: MethodArgsPtrTuple, + { async move { todo!("Implement the logic to call the method with pre-allocated argument pointers") } @@ -595,23 +601,33 @@ pub struct MockMethodInArgMaybeUninit { pub _phantom: core::marker::PhantomData, } +/// Runtime-specific concrete type for a fully-initialised Mock method argument pointer. +pub struct MockMethodInArgPtr { + _phantom: core::marker::PhantomData, +} + +impl MethodInArgPtr for MockMethodInArgPtr {} + impl MethodInArgMaybeUninit for MockMethodInArgMaybeUninit { - fn write(self, _val: T) -> MethodInArgPtr { - MethodInArgPtr { + type Ptr = MockMethodInArgPtr; + + fn write(self, _val: T) -> ZeroCopyArgs> { + ZeroCopyArgs(MockMethodInArgPtr { _phantom: core::marker::PhantomData, - } + }) } - unsafe fn assume_init(self) -> MethodInArgPtr { - MethodInArgPtr { + unsafe fn assume_init(self) -> ZeroCopyArgs> { + ZeroCopyArgs(MockMethodInArgPtr { _phantom: core::marker::PhantomData, - } + }) } } pub struct MockMethodInArgAllocator; impl MethodInArgAllocator for MockMethodInArgAllocator { + type MethodInArgPtr = MockMethodInArgPtr; type MethodInArgMaybeUninit = MockMethodInArgMaybeUninit; fn allocate(&self) -> MockMethodInArgMaybeUninit { MockMethodInArgMaybeUninit { diff --git a/score/mw/com/rust/design/design_document_method.md b/score/mw/com/rust/design/design_document_method.md index 29f061f61..ae69b2334 100644 --- a/score/mw/com/rust/design/design_document_method.md +++ b/score/mw/com/rust/design/design_document_method.md @@ -34,8 +34,8 @@ This document describes the design of the **method** APIs and usage of it. ## Overview -Methods implement a request/response communication pattern: a consumer calls a method on a producer with typed arguments and asynchronously awaits a typed return value. - +Rust Communication library provide the Method based communication pattern (mostly with alignment of c++ APIs), followings are major points +of the design- - Method calls are always async and every generated wrapper returns `impl Future` and must be `.await`ed. - Arguments can be passed by value (copy path) or via pre-allocated pointers (zero-copy path) using the same call site and the compiler selects the correct dispatch based on argument type. - Return values are wrapped in `R::MethodReturnSample`, which provides `Deref` access, allowing the runtime to back the return with shared memory without an extra copy. @@ -125,8 +125,10 @@ pub trait MethodCaller where Args: MethodArgsAllocate; - fn invoke_zero_copy<'a>(&'a self, ptrs: ::PtrTuple) - -> impl Future>> + 'a; + fn invoke_zero_copy<'a>(&'a self, ptrs: >::PtrTuple) + -> impl Future>> + 'a + where + Args: MethodArgsPtrTuple; } ``` @@ -138,12 +140,16 @@ These traits form the zero-copy argument allocation pipeline. #### `MethodReturnSample` -A runtime-defined wrapper for the method return value. It implements `Deref`, giving the caller read access to the returned data without an additional copy, this is similar to `Sample` for event data on the consumer side. +Trait for the return value of a method call on the consumer side. Mirrors `Sample` in the event design, each runtime implements this trait on its own concrete type, which can provide zero-copy access to the return data by backing it with shared memory. + +```rust +pub trait MethodReturnSample: Deref {} +``` -The `Runtime` trait declares it as an associated type because interface macro needs to access this and also it is runtime specific. +The `Runtime` trait declares it as an associated type bounded by this trait: ```rust -type MethodReturnSample: Deref; +type MethodReturnSample: MethodReturnSample; ``` Concrete implementations: @@ -157,15 +163,17 @@ let sample = consumer.get_tire_pressure().await?; let pressure: &Tire = &*sample; // Deref ``` -Once the LoLa FFI implementation is complete (issue #https://github.com/eclipse-score/communication/issues/782), `LolaMethodReturnSample` will reference a shared-memory slot rather than an owned copy. - #### `MethodInArgAllocator` -Runtime-specific allocator for method input arguments. An instance lives on the `MethodCaller` and hands out uninitialised slots. +Runtime-specific allocator for method input arguments. An instance lives on the `MethodCaller` and hands out uninitialised slots. It also declares the runtime's concrete pointer type (`MethodInArgPtr`) as an associated type, so both `write()` and `MethodArgsPtrTuple` resolve to the same concrete type. ```rust pub trait MethodInArgAllocator { - type MethodInArgMaybeUninit: MethodInArgMaybeUninit; + /// The runtime-specific concrete pointer type produced after initialisation. + type MethodInArgPtr: MethodInArgPtr; + + /// Equality constraint ensures write() returns ZeroCopyArgs>. + type MethodInArgMaybeUninit: MethodInArgMaybeUninit>; fn allocate(&self) -> Self::MethodInArgMaybeUninit; } @@ -173,29 +181,40 @@ pub trait MethodInArgAllocator { #### `MethodInArgMaybeUninit` -A single uninitialised argument slot. The caller writes a value into it, obtaining an initialised `MethodInArgPtr` that can be passed to the method call. +A single uninitialised argument slot. The caller writes a value into it, obtaining a `ZeroCopyArgs` that can be passed to the method call. Mirrors `SampleMaybeUninit` in the event design. ```rust pub trait MethodInArgMaybeUninit { - fn write(self, val: T) -> MethodInArgPtr; + /// Runtime-specific concrete pointer type + type Ptr: MethodInArgPtr; + + fn write(self, val: T) -> ZeroCopyArgs; /// # Safety /// The caller must ensure the memory has been properly initialized. - unsafe fn assume_init(self) -> MethodInArgPtr; + unsafe fn assume_init(self) -> ZeroCopyArgs; } ``` #### `MethodInArgPtr` -A pointer to a fully-initialised, pre-allocated method argument. It is used in the zero-copy call path instead of passing `T` by value. +Trait for a fully-initialised, pre-allocated method argument pointer. Mirrors `SampleMut` in the event design, each runtime implements this on its own concrete type (e.g. `LolaMethodInArgPtr`) which will store an FFI slot pointer and run RAII cleanup on `Drop` once shared-memory pointer layout is added (issue #781). ```rust -pub struct MethodInArgPtr { - pub _phantom: core::marker::PhantomData, -} +pub trait MethodInArgPtr {} ``` -> **Note**: `MethodInArgPtr` is currently a placeholder. Real shared-memory layout support is tracked in [issue #781](https://github.com/eclipse-score/communication/issues/781). +Runtime concrete types: +- `LolaMethodInArgPtr` - in `com-api-runtime-lola` (placeholder, `Drop` stub ready for issue #781) +- `MockMethodInArgPtr` - in `com-api-runtime-mock` + +#### `ZeroCopyArgs

` + +Newtype wrapper returned by `MethodInArgMaybeUninit::write()`. Passing a tuple of `ZeroCopyArgs

` to a consumer method selects the zero-copy call path. `P` is the runtime-specific type implementing `MethodInArgPtr`. + +```rust +pub struct ZeroCopyArgs

(pub P); +``` ### Macro-Internal Supporting Traits @@ -204,13 +223,22 @@ These traits are not implemented by runtimes. Blanket implementations are provid #### `MethodArgs` -Marker trait for method argument tuples. Carries `PtrTuple`- the matching tuple of `MethodInArgPtr` values used in the zero-copy path. +Marker trait for method argument tuples. Requires `CommData` because the tuple of argument values is the thing being transmitted in the copy path. + +```rust +pub trait MethodArgs: CommData {} +``` + +#### `MethodArgsPtrTuple` + +Maps an `Args` tuple to the matching `ZeroCopyArgs`-wrapped pointer tuple for a given runtime `R`. Separated from `MethodArgs` because pointer types are runtime-specific — they live in `R::MethodInArgAllocator::MethodInArgPtr`. ```rust -pub trait MethodArgs: CommData { +pub trait MethodArgsPtrTuple: MethodArgs { type PtrTuple; } -// e.g. (Tire, Tire)::PtrTuple = (MethodInArgPtr, MethodInArgPtr) +// e.g. for R = LolaRuntime: +// (Tire, Tire)::PtrTuple = (ZeroCopyArgs>, ZeroCopyArgs>) ``` #### `MethodArgsAllocate` @@ -227,7 +255,7 @@ pub trait MethodArgsAllocate: MethodArgs { #### `MethodCallInput` -Unified input type for the `interface!`- generated consumer method wrapper. Implemented for both `Args` (copy path) and `Args::PtrTuple` (zero-copy path). The compiler selects the correct impl from the type passed at the call site - no runtime branching. +Unified input type for the `interface!`-generated consumer method wrapper. Implemented for both `Args` (copy path) and `(ZeroCopyArgs, ...)` (zero-copy path). The compiler selects the correct impl from the type passed at the call site — no runtime branching. ```rust pub trait MethodCallInput { @@ -241,8 +269,8 @@ pub trait MethodCallInput` @@ -265,12 +293,13 @@ The macro generates blanket implementations of the following traits for each ari | Trait | Why blanket | |-------|-------------| -| `MethodArgs` | `PtrTuple` construction per arity | +| `MethodArgs` | Marker — arg tuple is `CommData` if all elements are | +| `MethodArgsPtrTuple` | `PtrTuple` construction per arity using `R::MethodInArgAllocator::MethodInArgPtr` | | `MethodArgsAllocate` | `alloc_uninit` loops per arity | -| `MethodCallInput` | Zero-copy path dispatches per arity | +| `MethodCallInput` | Zero-copy path dispatches per arity via `ZeroCopyArgs` tuples | | `MethodHandlerCall` | Handler `call()` unpacks tuple per arity | | `Reloc` | Arg tuple is relocatable if all elements are | -| `CommData` | Arg tuple is `Communication data` if all elements are | +| `CommData` | Arg tuple is `CommData` if all elements are | The result: adding a new runtime requires only implementing `MethodCaller` and `MethodHandler`. All arity-specific glue is already provided. @@ -293,9 +322,9 @@ consumer.update_tire_pressure(tire).await?; ```rust // Allocate uninit slots from the runtime's MethodInArgAllocator let (uninit,) = consumer.update_tire_pressure.allocate()?; -// Write the value into the slot, get back an initialised pointer +// Write the value into the slot — returns ZeroCopyArgs> let tire_ptr = uninit.write(Tire { pressure: 35.0 }); -// PtrTuple = (MethodInArgPtr,) - MethodCallInput impl for PtrTuple - invoke_zero_copy +// PtrTuple = (ZeroCopyArgs>,) — MethodCallInput zero-copy impl — invoke_zero_copy consumer.update_tire_pressure(tire_ptr).await?; ``` @@ -476,9 +505,9 @@ All `LolaMethodCaller` and `LolaMethodHandler` methods in `com-api-runtime-lola/ See: -### Issue #781-Replace `MethodInArgPtr` with a real shared-memory pointer +### Issue #781 — Implement RAII lifecycle on `MethodInArgPtr` concrete types -`MethodInArgPtr` is currently a `PhantomData` placeholder with no memory backing. Once the runtime implements zero-copy method arguments over shared memory, this type needs to carry a real pointer or reference to the allocated slot, with appropriate lifetime and safety constraints-analogous to `SampleMut` for events. +`MethodInArgPtr` is now a trait (not a struct). Each runtime has its own concrete type (`LolaMethodInArgPtr`, `MockMethodInArgPtr`) that currently holds only `PhantomData`. Once shared-memory layout is added, these types need to store a real FFI slot pointer and implement `Drop` to release the slot if `invoke_zero_copy` is never called — analogous to `AllocateePtrWrapper` / `LolaBinding` in the event design. See: diff --git a/score/mw/com/rust/design/method_trait_diagram.puml b/score/mw/com/rust/design/method_trait_diagram.puml index d730a6d32..511f5b15e 100644 --- a/score/mw/com/rust/design/method_trait_diagram.puml +++ b/score/mw/com/rust/design/method_trait_diagram.puml @@ -39,14 +39,30 @@ interface MethodCaller { + new(method_name, instance_info) -> Result + invoke_with_copy(args: Args) -> impl Future>> + allocate() -> Result - + invoke_zero_copy(ptrs: Args::PtrTuple) -> impl Future>> + + invoke_zero_copy(ptrs: >::PtrTuple) -> impl Future>> +} + +interface "MethodReturnSample" as MethodReturnSample { + --- + ' Trait - mirrors Sample in the event design + ' Bound: Deref + ' Runtime implements on its concrete type: + ' LolaMethodReturnSample, MockMethodReturnSample } interface MethodArgs { + --- + ' Marker trait: CommData supertrait + ' Blanket impls for arities 0-8 via impl_all_arities!() + ' No PtrTuple - pointer types are runtime-specific (see MethodArgsPtrTuple) +} + +interface "MethodArgsPtrTuple" as MethodArgsPtrTuple { type PtrTuple --- ' Blanket impls for arities 0-8 via impl_all_arities!() - ' PtrTuple = (MethodInArgPtr, ..., MethodInArgPtr) + ' PtrTuple = (ZeroCopyArgs>, ...) + ' Separated from MethodArgs because pointer types are runtime-specific } interface "MethodArgsAllocate" as MethodArgsAllocate { @@ -60,9 +76,8 @@ interface "MethodCallInput" as MethodCallInput { --- + invoke(caller: &R::MethodCaller) -> impl Future>> --- - ' Copy path: impl for Args itself (dispatches invoke_with_copy) - ' Zero-copy path: impl for Args::PtrTuple (dispatches invoke_zero_copy) - ' Both impls are blanket - no per-arity code in runtimes + ' Copy path: impl for Args itself (dispatches invoke_with_copy) + ' Zero-copy path: impl for (ZeroCopyArgs, ...) (dispatches invoke_zero_copy) } interface "MethodHandlerCall" as MethodHandlerCall { @@ -74,28 +89,37 @@ interface "MethodHandlerCall" as MethodHandlerCall { } interface "MethodInArgAllocator" as MethodInArgAllocator { + type MethodInArgPtr type MethodInArgMaybeUninit --- + allocate() -> MethodInArgMaybeUninit + --- + ' MethodInArgPtr declared here so both write() and + ' MethodArgsPtrTuple resolve to the same concrete type } interface "MethodInArgMaybeUninit" as MethodInArgMaybeUninit { + type Ptr: MethodInArgPtr + --- + + write(val: T) -> ZeroCopyArgs + + assume_init() -> ZeroCopyArgs --- - + write(val: T) -> MethodInArgPtr - + assume_init() -> MethodInArgPtr + ' Mirrors SampleMaybeUninit in the event design } -class "MethodReturnSample" as MethodReturnSample { +interface "MethodInArgPtr" as MethodInArgPtr { --- - ' Wraps method return value, provides Deref access - ' Allows zero-copy return from shared memory (like Sample for events) - ' Concrete types: LolaMethodReturnSample, MockMethodReturnSample -} + ' Trait - mirrors SampleMut in the event design + ' Runtime implements on its concrete type: + ' LolaMethodInArgPtr, MockMethodInArgPtr + ' Concrete type will hold FFI slot pointer + Drop -class "MethodInArgPtr" as MethodInArgPtr { +class "ZeroCopyArgs

" as ZeroCopyArgs { + + 0: P --- - ' Placeholder for pre-allocated shared-memory argument pointer - ' Real layout implementation pending (issue #781) + ' Newtype wrapper returned by write() + ' Does NOT implement CommData - keeps MethodCallInput impls disjoint + ' User never writes this type explicitly; always inferred from write() } Runtime --> MethodCaller : defines as\nassociated type @@ -103,20 +127,21 @@ Runtime --> MethodHandler : defines as\nassociated type Runtime --> MethodInArgAllocator : defines as\nassociated type Runtime --> MethodReturnSample : defines as\nassociated type -MethodCaller --> MethodArgs : requires Args bound MethodCaller --> MethodArgsAllocate : uses for allocate() +MethodCaller --> MethodArgsPtrTuple : uses for invoke_zero_copy() MethodCaller --> MethodInArgAllocator : via Runtime associated type MethodCaller --> MethodReturnSample : invoke returns MethodHandler --> MethodHandlerCall : accepts in\nregister_handler() MethodArgsAllocate --|> MethodArgs : extends +MethodArgsPtrTuple --|> MethodArgs : extends MethodInArgAllocator --> MethodInArgMaybeUninit : allocate() produces +MethodInArgAllocator --> MethodInArgPtr : declares as\nassociated type -MethodInArgMaybeUninit --> MethodInArgPtr : write() returns - -MethodArgs --> MethodInArgPtr : PtrTuple\ncomposed of +MethodInArgMaybeUninit --> ZeroCopyArgs : write() returns +ZeroCopyArgs --> MethodInArgPtr : wraps MethodCallInput --> MethodCaller : invoke() dispatches to\ninvoke_with_copy or\ninvoke_zero_copy diff --git a/score/mw/com/rust/design/method_trait_diagram.svg b/score/mw/com/rust/design/method_trait_diagram.svg index 8f988b4ae..2295540ca 100644 --- a/score/mw/com/rust/design/method_trait_diagram.svg +++ b/score/mw/com/rust/design/method_trait_diagram.svg @@ -1 +1 @@ -Runtimetype MethodCaller<Args, Return>type MethodHandler<Args, Return>type MethodInArgAllocatortype MethodReturnSample<T: CommData>find_service()producer_builder()MethodHandlerArgs: MethodArgs, Return: CommData, R: Runtimenew(method_name, instance_info) -> Result<Self>register_handler(handler: impl MethodHandlerCall<Args, Return>)MethodCallerArgs: MethodArgs, Return: CommData, R: Runtimenew(method_name, instance_info) -> Result<Self>invoke_with_copy(args: Args) -> impl Future<Output = Result<R::MethodReturnSample<Return>>>allocate() -> Result<Args::UninitTuple>invoke_zero_copy(ptrs: Args::PtrTuple) -> impl Future<Output = Result<R::MethodReturnSample<Return>>>MethodArgstype PtrTupleMethodArgsAllocateA: MethodInArgAllocatortype UninitTuplealloc_uninit(allocator: &A) -> UninitTupleMethodCallInputArgs, Return, Rinvoke(caller: &R::MethodCaller<Args, Return>) -> impl Future<Output = Result<R::MethodReturnSample<Return>>>MethodHandlerCallArgs, Returncall(args: Args) -> ReturnMethodInArgAllocatortype MethodInArgMaybeUninit<T: CommData>allocate<T: CommData>() -> MethodInArgMaybeUninit<T>MethodInArgMaybeUninitTwrite(val: T) -> MethodInArgPtr<T>assume_init() -> MethodInArgPtr<T>MethodReturnSampleTMethodInArgPtrTdefines asassociated typedefines asassociated typedefines asassociated typedefines asassociated typerequires Args bounduses for allocate()via Runtime associated typeinvoke returnsaccepts inregister_handler()extendsallocate() produceswrite() returnsPtrTuplecomposed ofinvoke() dispatches toinvoke_with_copy orinvoke_zero_copy \ No newline at end of file +Runtimetype MethodCaller<Args, Return>type MethodHandler<Args, Return>type MethodInArgAllocatortype MethodReturnSample<T: CommData>find_service()producer_builder()MethodHandlerArgs: MethodArgs, Return: CommData, R: Runtimenew(method_name, instance_info) -> Result<Self>register_handler(handler: impl MethodHandlerCall<Args, Return>)MethodCallerArgs: MethodArgs, Return: CommData, R: Runtimenew(method_name, instance_info) -> Result<Self>invoke_with_copy(args: Args) -> impl Future<Output = Result<R::MethodReturnSample<Return>>>allocate() -> Result<Args::UninitTuple>invoke_zero_copy(ptrs: <Args as MethodArgsPtrTuple<R>>::PtrTuple) -> impl Future<Output = Result<R::MethodReturnSample<Return>>>MethodReturnSampleTMethodArgsMethodArgsPtrTupleR: Runtimetype PtrTupleMethodArgsAllocateA: MethodInArgAllocatortype UninitTuplealloc_uninit(allocator: &A) -> UninitTupleMethodCallInputArgs, Return, Rinvoke(caller: &R::MethodCaller<Args, Return>) -> impl Future<Output = Result<R::MethodReturnSample<Return>>>MethodHandlerCallArgs, Returncall(args: Args) -> ReturnMethodInArgAllocatortype MethodInArgPtr<T: CommData>type MethodInArgMaybeUninit<T: CommData>allocate<T: CommData>() -> MethodInArgMaybeUninit<T>MethodInArgMaybeUninitTtype Ptr: MethodInArgPtr<T>write(val: T) -> ZeroCopyArgs<Self::Ptr>assume_init() -> ZeroCopyArgs<Self::Ptr>MethodInArgPtrTZeroCopyArgsP0: Pdefines asassociated typedefines asassociated typedefines asassociated typedefines asassociated typeuses for allocate()uses for invoke_zero_copy()via Runtime associated typeinvoke returnsaccepts inregister_handler()extendsextendsallocate() producesdeclares asassociated typewrite() returnswrapsinvoke() dispatches toinvoke_with_copy orinvoke_zero_copy \ No newline at end of file diff --git a/score/mw/com/rust/score_com.rs b/score/mw/com/rust/score_com.rs index 4cfd6b429..324edc2f1 100644 --- a/score/mw/com/rust/score_com.rs +++ b/score/mw/com/rust/score_com.rs @@ -143,11 +143,12 @@ pub use com_api_runtime_lola::RuntimeBuilderImpl as LolaRuntimeBuilderImpl; pub use score_com_concept::{ interface, interface_common, interface_consumer, interface_producer, Builder, CommData, Consumer, ConsumerBuilder, ConsumerDescriptor, Error, FindServiceSpecifier, HandlerNotSet, - HandlerSet, InstanceSpecifier, Interface, MethodArgs, MethodArgsAllocate, MethodCallInput, - MethodCaller, MethodHandler, MethodHandlerCall, MethodInArgAllocator, MethodInArgMaybeUninit, - MethodInArgPtr, OfferedProducer, PlacementDefault, Producer, ProducerBuilder, ProviderInfo, - Publisher, Reloc, Result, Runtime, RuntimeBuilder, SampleContainer, SampleMaybeUninit, - SampleMut, ServiceDiscovery, Subscriber, Subscription, + HandlerSet, InstanceSpecifier, Interface, MethodArgs, MethodArgsAllocate, MethodArgsPtrTuple, + MethodCallInput, MethodCaller, MethodHandler, MethodHandlerCall, MethodInArgAllocator, + MethodInArgMaybeUninit, MethodInArgPtr, MethodReturnSample, OfferedProducer, PlacementDefault, + Producer, ProducerBuilder, ProviderInfo, Publisher, Reloc, Result, Runtime, RuntimeBuilder, + SampleContainer, SampleMaybeUninit, SampleMut, ServiceDiscovery, Subscriber, Subscription, + ZeroCopyArgs, }; #[doc(hidden)] diff --git a/score/mw/com/rust/score_com_concept/concept.rs b/score/mw/com/rust/score_com_concept/concept.rs index 829668a10..39326f84b 100644 --- a/score/mw/com/rust/score_com_concept/concept.rs +++ b/score/mw/com/rust/score_com_concept/concept.rs @@ -49,7 +49,9 @@ //! - Tuples use crate::error::*; -use crate::method_concept::*; +use crate::method_concept::{ + MethodArgs, MethodCaller, MethodHandler, MethodInArgAllocator, MethodReturnSample, +}; use crate::Reloc; use containers::fixed_capacity::FixedCapacityQueue; use core::fmt::Debug; @@ -104,9 +106,7 @@ pub trait Runtime { type MethodInArgAllocator: MethodInArgAllocator; /// `MethodReturnSample` wraps the return value of a method call. - /// It provides `Deref` access to the return data, similar to`Sample` for - /// events, allowing the runtime to back the return value with shared memory without copying. - type MethodReturnSample: Deref; + type MethodReturnSample: MethodReturnSample; /// `MethodCaller` types for calling methods on the proxy/consumer side type MethodCaller: MethodCaller; @@ -224,8 +224,7 @@ pub trait CommData: Reloc { const ID: &'static str; } -// Arity-0 unit tuple — special-cased here; arities 1+ are generated by -// `impl_all_arities!` in `method_arities.rs`. +// Arity-0 unit tuple hereand other arities 1+ are generated by `impl_all_arities!` in `method_arities.rs`. impl CommData for () { const ID: &'static str = "()"; } diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_macros.rs index 2054ab6ac..deb55c6ce 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -78,6 +78,12 @@ pub struct HandlerSet; /// ``` /// The generated code will include: /// - `VehicleInterface` struct with `INTERFACE_ID = "abc::Vehicle"` +/// - `VehicleConsumer` struct that implements `Consumer` trait for subscribing to +/// "left_tire" and "exhaust" events. +/// - `VehicleProducer` struct that implements `Producer` trait for producing +/// "left_tire" and "exhaust" events. +/// - `VehicleOfferedProducer` struct that implements `OfferedProducer` trait for offering +/// "left_tire" and "exhaust" events. /// - `VehicleConsumer`, `VehicleProducer`, `VehicleOfferedProducer` /// /// # Example: Mixed interface (Event + Field + Method) with custom ID @@ -128,14 +134,14 @@ pub struct HandlerSet; /// panic at runtime, since the handlers have not been registered yet. #[macro_export] macro_rules! interface { - // Backward-compatible: Event-only, auto-generated ID + // Default unique ID based on the module path and interface name (interface $id:ident { $($event_name:ident : Event<$event_type:ty>),+ $(,)? }) => { $crate::interface_common!($id); $crate::interface_consumer!($id, $($event_name, Event<$event_type>),+); $crate::interface_producer!($id, $($event_name, Event<$event_type>),+); }; - // Backward-compatible: Event-only, custom ID + // Custom unique Id provided by the user (interface $id:ident { Id = $uid:expr, $($event_name:ident : Event<$event_type:ty>),+ $(,)? @@ -145,7 +151,7 @@ macro_rules! interface { $crate::interface_producer!($id, $($event_name, Event<$event_type>),+); }; - // Backward-compatible: Event-only with comma separator (legacy syntax) + // This is for backward compatibility for existing users with comma (,) (interface $id:ident, { Id = $uid:expr, $($event_name:ident : Event<$event_type:ty>),+ $(,)? @@ -158,7 +164,7 @@ macro_rules! interface { } }; - // Mixed / unified: custom ID - MUST come before auto-ID catch-all + // Mixed / unified: custom ID (interface $id:ident { Id = $uid:expr, $($members:tt)* @@ -173,7 +179,7 @@ macro_rules! interface { ); }; - // Mixed / unified: auto-generated ID - catch-all, must come last. + // Mixed / unified: auto-generated ID (interface $id:ident { $($members:tt)* }) => { $crate::interface_common!($id); $crate::_interface_collect_members!( @@ -189,14 +195,6 @@ macro_rules! interface { /// Internal recursive-macro helper for `interface!`. /// /// Accumulates members into three typed lists, then calls the mixed generator macros. -/// -/// Accumulator format: -/// ```text -/// @id[$id, $uid] -/// @ev[$($ev_name : $ev_type ,)*] -/// @fi[$($fi_name : $fi_type ,)*] -/// @me[$($me_name ($me_args) -> $me_ret ,)*] -/// ``` #[doc(hidden)] #[macro_export] macro_rules! _interface_collect_members { @@ -889,7 +887,7 @@ mod tests { /// Mixed interface (Event + Field + Method) with a custom ID. /// - /// ```ignore + /// ``` /// mod my_module { /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; /// @@ -900,18 +898,11 @@ mod tests { /// const ID: &'static str = "Tire"; /// } /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// /// interface!( /// interface Vehicle { /// Id = "AbcInterface", /// left_tire: Event, - /// left_tire_field: Field, + /// left_tire_field: Field, /// left_tire_method(Tire) -> Tire, /// } /// ); @@ -920,13 +911,13 @@ mod tests { /// Generates `VehicleInterface`, `VehicleConsumer`, `VehicleProducer`, /// and `VehicleOfferedProducer` where: /// - `VehicleConsumer` has `left_tire: Subscriber`, - /// `left_tire_field: FieldSubscriber`, + /// `left_tire_field: FieldSubscriber`, /// `left_tire_method: MethodCaller<(Tire,), Tire>`, /// and a convenience `left_tire_method(arg0: Tire)` method. /// - `VehicleProducer` derives `TypeStateValidator` and requires the `.init()` chain: /// `producer.init().update_left_tire_field(&val)?.register_set_handler_left_tire_field(f).register_left_tire_method_handler(h).offer()?` /// - `VehicleOfferedProducer` has `left_tire: Publisher` (created lazily on offer), - /// `left_tire_field: FieldPublisher`, plus the active method handler. + /// `left_tire_field: FieldPublisher`, plus the active method handler. #[cfg(doctest)] fn interface_macro_mixed() {} @@ -1621,97 +1612,4 @@ mod validation_tests { } test_module::validate(); } - - #[test] - fn test_mixed_interface_types_generated() { - mod test_module { - use score_com::{ - CommData, Interface, LolaRuntimeImpl as LolaRuntime, ProviderInfo, Publisher, - Reloc, Subscriber, - }; - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Tire { - pub pressure: f32, - } - impl CommData for Tire { - const ID: &'static str = "Tire"; - } - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Exhaust {} - impl CommData for Exhaust { - const ID: &'static str = "Exhaust"; - } - - crate::interface!( - interface VehicleMixed { - Id = "VehicleMixedInterface", - left_tire: Event, - exhaust_field: Field, - update_pressure(Tire) -> Tire, - } - ); - - pub fn validate() { - // Verify custom interface ID. - let interface_id = ::INTERFACE_ID; - assert_eq!(interface_id, "VehicleMixedInterface"); - - // Verify all four types are generated with correct names. - let _ = core::marker::PhantomData::; - let _ = core::marker::PhantomData::>; - let _ = core::marker::PhantomData::>; - let _ = core::marker::PhantomData::>; - - // Verify Consumer struct size (confirms fields were generated). - assert!( - std::mem::size_of::>() > 0, - "VehicleMixedConsumer should have member fields" - ); - } - } - test_module::validate(); - } - - #[test] - fn test_mixed_interface_event_only_via_recursive_macro() { - // Verifies that a mixed-arm interface with only events still generates - // the same types as the backward-compatible Event-only arm. - mod test_module { - use score_com::{CommData, Interface, LolaRuntimeImpl as LolaRuntime, Reloc}; - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Signal { - pub value: u32, - } - impl CommData for Signal { - const ID: &'static str = "Signal"; - } - - // This goes through the recursive-macro path (mixed arm) but with only events. - crate::interface!( - interface Radar { - target: Event, - velocity: Event, - } - ); - - pub fn validate() { - let interface_id = ::INTERFACE_ID; - assert_eq!( - interface_id, - concat!(module_path!(), "::", "Radar"), - "Auto-generated ID should include module path" - ); - let _ = core::marker::PhantomData::>; - let _ = core::marker::PhantomData::>; - let _ = core::marker::PhantomData::>; - } - } - test_module::validate(); - } } diff --git a/score/mw/com/rust/score_com_concept/method_arities_macros.rs b/score/mw/com/rust/score_com_concept/method_arities_macros.rs index 0d170abed..f92b65f47 100644 --- a/score/mw/com/rust/score_com_concept/method_arities_macros.rs +++ b/score/mw/com/rust/score_com_concept/method_arities_macros.rs @@ -34,8 +34,8 @@ //! This macro covers arities 1 through 8 (inclusive) by default, but can be extended to higher arities if needed. use crate::{ - CommData, MethodArgs, MethodArgsAllocate, MethodCallInput, MethodCaller, MethodHandlerCall, - MethodInArgAllocator, MethodInArgPtr, Reloc, Result, Runtime, + CommData, MethodArgs, MethodArgsAllocate, MethodArgsPtrTuple, MethodCallInput, MethodCaller, + MethodHandlerCall, MethodInArgAllocator, Reloc, Result, Runtime, ZeroCopyArgs, }; use core::future::Future; @@ -60,8 +60,15 @@ macro_rules! impl_all_arities { const ID: &'static str = stringify!(($($T,)* $nextT,)); } - impl<$($T: CommData,)* $nextT: CommData> MethodArgs for ($($T,)* $nextT,) { - type PtrTuple = ($( MethodInArgPtr<$T>, )* MethodInArgPtr<$nextT>,); + impl<$($T: CommData,)* $nextT: CommData> MethodArgs for ($($T,)* $nextT,) {} + + impl<$($T: CommData,)* $nextT: CommData, R: Runtime + ?Sized> + MethodArgsPtrTuple for ($($T,)* $nextT,) + { + type PtrTuple = ( + $( ZeroCopyArgs<::MethodInArgPtr<$T>>, )* + ZeroCopyArgs<::MethodInArgPtr<$nextT>>, + ); } impl<$($T: CommData,)* $nextT: CommData, _Alloc: MethodInArgAllocator> @@ -77,15 +84,20 @@ macro_rules! impl_all_arities { } } - // Accepts a tuple of `MethodInArgPtr` values and dispatches to - // `invoke_zero_copy`. The copy path is already covered by the blanket impl - // in `com_api_method.rs` and does not need to be repeated here. + // Accepts a tuple of `ZeroCopyArgs>` values and dispatches to + // `invoke_zero_copy`. The `Ptr = Self::MethodInArgPtr` constraint on MethodInArgAllocator + // ensures these types unify with what `write()` returns. + // The copy path is covered by the arity-agnostic blanket impl in `method_concept.rs`. impl<$($T: CommData,)* $nextT: CommData, Return: CommData, R: Runtime + ?Sized> MethodCallInput<($($T,)* $nextT,), Return, R> - for ($( MethodInArgPtr<$T>, )* MethodInArgPtr<$nextT>,) + for ($( ZeroCopyArgs<::MethodInArgPtr<$T>>, )* ZeroCopyArgs<::MethodInArgPtr<$nextT>>,) where R::MethodCaller<($($T,)* $nextT,), Return>: MethodCaller<($($T,)* $nextT,), Return, R>, + // Equality constraint: lets the compiler unify Self with PtrTuple. + ($($T,)* $nextT,): MethodArgsPtrTuple::MethodInArgPtr<$T>>, )* ZeroCopyArgs<::MethodInArgPtr<$nextT>>,) + >, { fn invoke<'a>( self, @@ -95,22 +107,18 @@ macro_rules! impl_all_arities { R::MethodCaller<($($T,)* $nextT,), Return>: MethodCaller<($($T,)* $nextT,), Return, R> + 'a, { - // Destructure with positional arg names, then reconstruct the ptr tuple. - #[allow(non_snake_case)] - let ($($a,)* $nextA,) = self; + // `self` IS >::PtrTuple by the equality + // constraint above, so pass it directly to invoke_zero_copy. as MethodCaller<($($T,)* $nextT,), Return, R>>::invoke_zero_copy( caller, - ($($a,)* $nextA,), + self, ) } } - - // Maps a plain `Fn(T1, T2, …) -> Return` closure to the tuple-based call - // convention used by the runtime. - impl<_F, $($T,)* $nextT, Return> MethodHandlerCall<($($T,)* $nextT,), Return> for _F + impl MethodHandlerCall<($($T,)* $nextT,), Return> for F where - _F: Fn($($T,)* $nextT,) -> Return + Send + Sync + 'static, + F: Fn($($T,)* $nextT,) -> Return + Send + Sync + 'static, { fn call(&self, args: ($($T,)* $nextT,)) -> Return { #[allow(non_snake_case)] diff --git a/score/mw/com/rust/score_com_concept/method_concept.rs b/score/mw/com/rust/score_com_concept/method_concept.rs index 596c501af..12dfecd40 100644 --- a/score/mw/com/rust/score_com_concept/method_concept.rs +++ b/score/mw/com/rust/score_com_concept/method_concept.rs @@ -44,12 +44,20 @@ /// it is used in the zero-copy method call path. /// Which provide the allocate API for specific argument type and /// return the uninitialized method argument type for that argument type. +/// `MethodReturnSample`: This is a trait for the return value of a method call on the consumer side, +/// it is used to provide `Deref` access to the return value, +/// `MethodInArgPtr`: This is a trait for the pointer type for a single method argument, +/// it is used in the zero-copy method call path. +/// `ZeroCopyArgs

`: Newtype wrapper for the zero-copy dispatch, returned by `MethodInArgMaybeUninit::write()` /// /// Now below traits are marker / marker-like (because it is implemented for all supported arities) traits and /// which no need to implement by runtime because blanket implementation is added in this crate. /// /// `MethodArgs`: Marker trait for method argument tuples, -/// this is used to carry the matching tuple of MethodInArgPtr used in the zero-copy call path. +/// no need to implement by runtime because blanket implementation is added in this crate. +/// `MethodArgsPtrTuple`: Maps an Args tuple to the matching ZeroCopyArgs-wrapped pointer tuple for a given runtime R, +/// this is used to provide the PtrTuple type for the zero-copy call path, +/// it is a separate trait from MethodArgs because pointer types are runtime-specific. /// `MethodArgsAllocate`: Maps an Args tuple type to the matching uninitialized method argument tuple for a specific runtime allocator A, /// this is used to produce the uninitialized method arguments for zero-copy method call path. /// `MethodCallInput`: Unified input for a method call accepted by the interface macro-generated consumer methods, @@ -70,16 +78,22 @@ /// // TODO: Add a blocking `.wait()` convenience for method-call futures, for sync callers who don't // want to bring their own async executor (similar to `futures::executor::block_on`). -use crate::concept::*; +use crate::concept::{CommData, Result, Runtime}; use core::future::Future; +use core::ops::Deref; -// This is a pointer type for a pre-allocated method argument. It is used in the zero-copy method call path. -// TODO: Remove this once memory layout implementation is added in rust side, same like samplePtr. -// Also need to check about lifetime of this pointer and add all the trait or type which is required. -// https://github.com/eclipse-score/communication/issues/781 -pub struct MethodInArgPtr { - pub _phantom: core::marker::PhantomData, -} +/// Trait for the return value of a method call on the consumer side. +// Mirrors `Sample` in the event design +pub trait MethodReturnSample: Deref {} + +/// Marker trait for a fully-initialised, pre-allocated method argument. +/// Runtimes implement this with a concrete type that stores an FFI pointer +pub trait MethodInArgPtr {} + +/// Newtype wrapper returned by `MethodInArgMaybeUninit::write()`. +/// Passing a tuple of `ZeroCopyArgs

` to a consumer method selects the zero-copy call path. +/// `P` is the runtime-specific type that implements `MethodInArgPtr`. +pub struct ZeroCopyArgs

(pub P); /// Producer side registration of method handlers. /// This is the interface that a producer implements to register handlers for its methods. @@ -161,14 +175,18 @@ pub trait MethodCaller /// Invoke the method with zero-copy arguments. This is the zero-copy path for method calls. /// /// # Arguments - /// * `ptrs` - The pre-allocated method argument pointers to pass to the method call in a tuple. + /// * `ptrs` - A tuple of `ZeroCopyArgs>`-wrapped pointers, one per + /// method argument. The runtime is responsible for destructuring the `ZeroCopyArgs` + /// wrappers to access the inner runtime-specific pointer. /// /// Returns a future that resolves to a `Result` containing a `MethodReturnSample` /// which provides `Deref` access to the return value. fn invoke_zero_copy<'a>( &'a self, - ptrs: ::PtrTuple, - ) -> impl Future>> + 'a; + ptrs: >::PtrTuple, + ) -> impl Future>> + 'a + where + Args: MethodArgsPtrTuple; } /// This is the uninitialized type for a single method argument. It is used in the zero-copy method call path. @@ -180,21 +198,31 @@ pub trait MethodCaller /// method is called. A user can call `assume_init()` on an unwritten method argument, which /// is undefined behaviour once real shared memory backs these method arguments. pub trait MethodInArgMaybeUninit { - /// Write a value into this pre-allocated method argument and return the initialized pointer. - fn write(self, val: T) -> MethodInArgPtr; + /// The runtime-specific concrete pointer type produced after initialisation. + // Mirrors `SampleMaybeUninit::SampleMut` in the event design. + type Ptr: MethodInArgPtr; - /// Assume the method argument is already initialized and return the pointer. + /// Write a value into this pre-allocated method argument slot and return the initialised pointer. + fn write(self, val: T) -> ZeroCopyArgs; + + /// Assume the method argument slot is already initialised and return the pointer. /// /// # Safety /// The caller must ensure the memory has been properly initialized before calling this. - unsafe fn assume_init(self) -> MethodInArgPtr; + unsafe fn assume_init(self) -> ZeroCopyArgs; } /// This is for runtime-specific method argument allocation. It is used in the zero-copy method call path. /// This trait provides the allocate API for specific argument type and return the uninitialized method argument type for that argument type. pub trait MethodInArgAllocator { + /// The runtime-specific concrete pointer type this allocator's uninit slots produce after initialisation. + type MethodInArgPtr: MethodInArgPtr; + /// The concrete uninitialized method argument type this allocator produces for argument type `T`. - type MethodInArgMaybeUninit: MethodInArgMaybeUninit; + type MethodInArgMaybeUninit: MethodInArgMaybeUninit< + T, + Ptr = Self::MethodInArgPtr, + >; /// Produce a new uninitialized method argument for argument type `T`. /// @@ -208,18 +236,34 @@ pub trait MethodInArgAllocator { /// Marker trait for method argument tuples. /// -/// Carries `PtrTuple` - the matching tuple of `MethodInArgPtr` used in the zero-copy call path. -/// For example, `(Tire, Tire)::PtrTuple = (MethodInArgPtr, MethodInArgPtr)`. -/// /// Runtimes do not implement this trait. /// Blanket impls for all supported arities (0–8 arguments) are provided in this crate. -pub trait MethodArgs: CommData { +pub trait MethodArgs: CommData {} + +// Arity-0 unit tuple - special-cased here, arities 1+ are generated by +// `impl_all_arities!` in `method_arities_macros.rs`. +impl MethodArgs for () {} + +/// Maps an `Args` tuple to the matching `ZeroCopyArgs`-wrapped pointer tuple for a given runtime `R`. +/// +/// For runtime `R` and args `(Tire, Tire)`, `PtrTuple` becomes +/// `(ZeroCopyArgs>, ZeroCopyArgs>)` — +/// the types that the user produces from `write()` and passes to the consumer method, +/// and that `MethodCaller::invoke_zero_copy` receives directly. +/// +/// This is a separate runtime-parameterised trait because pointer types are runtime-specific, +/// they live in `R::MethodInArgPtr`, not in the concept crate. +/// +/// Runtimes do not implement this trait. +/// Blanket impls for all supported arities are generated by `impl_all_arities!` in `method_arities_macros.rs`. +pub trait MethodArgsPtrTuple: MethodArgs { + /// The tuple of runtime-specific pointer types for this args tuple. type PtrTuple; } -// Arity-0 unit tuple - special-cased here; arities 1+ are generated by -// `impl_all_arities!` in `method_arities.rs`. -impl MethodArgs for () { +// Arity-0 unit tuple - zero args means no pointers, regardless of runtime. +// Arities 1+ are generated by `impl_all_arities!` in `method_arities_macros.rs`. +impl MethodArgsPtrTuple for () { type PtrTuple = (); } @@ -251,7 +295,7 @@ impl MethodArgsAllocate for () { /// Allows the `interface!` macro to generate exactly a single consumer method per interface method /// instead of two separate copy and zero-copy methods. Implemented for: /// - `Args` itself - dispatches to `invoke_with_copy` (copy path) -/// - `MethodInArgPtr,...` - dispatches to `invoke_zero_copy` (zero-copy path) +/// - `(ZeroCopyArgs, ...)` - dispatches to `invoke_zero_copy` (zero-copy path) /// /// The compiler selects the right impl purely from the type passed at the call site, no runtime branching. /// @@ -275,7 +319,7 @@ pub trait MethodCallInput MethodCallInput for Args where Args: MethodArgs + CommData, diff --git a/score/mw/com/rust/score_com_concept/reloc.rs b/score/mw/com/rust/score_com_concept/reloc.rs index 2bce7f8a3..777f1dbcb 100644 --- a/score/mw/com/rust/score_com_concept/reloc.rs +++ b/score/mw/com/rust/score_com_concept/reloc.rs @@ -54,3 +54,5 @@ unsafe impl Reloc for [T; N] {} // MaybeUninit unsafe impl Reloc for core::mem::MaybeUninit {} + +// Tuples generated by `impl_all_arities!` in `method_arities_macros.rs` From 058e74f9d81318f467c889e7a88be148a1757ce4 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Wed, 29 Jul 2026 11:28:23 +0530 Subject: [PATCH 11/25] Rust::com Create the Field APIs * Created field interface APIs * Updated SampleMut to use in field as well --- .../mw/com/rust/score_com_concept/concept.rs | 19 ++- .../rust/score_com_concept/field_concept.rs | 145 +++++++++++++++++ .../score_com_concept/interface_macros.rs | 146 +++++++++++++++++- score/mw/com/rust/score_com_concept/lib.rs | 2 + 4 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 score/mw/com/rust/score_com_concept/field_concept.rs diff --git a/score/mw/com/rust/score_com_concept/concept.rs b/score/mw/com/rust/score_com_concept/concept.rs index 8e5f7601b..48b93f872 100644 --- a/score/mw/com/rust/score_com_concept/concept.rs +++ b/score/mw/com/rust/score_com_concept/concept.rs @@ -49,13 +49,14 @@ //! - Tuples use crate::error::*; +use crate::field_concept::{FieldPublisher, FieldSubscriber}; use crate::Reloc; -pub use score_com_macros::CommData; use containers::fixed_capacity::FixedCapacityQueue; use core::fmt::Debug; use core::future::Future; use core::ops::{Deref, DerefMut}; use futures::stream::Stream; +pub use score_com_macros::CommData; use std::path::Path; /// Result type alias with `std::result::Result` using `score_com::Error` as error type @@ -100,6 +101,12 @@ pub trait Runtime { /// `Publisher` types for Publishes event data to subscribers type Publisher: Publisher; + /// `FieldSubscription` types for Manages subscriptions to field instance + type FieldSubscriber: FieldSubscriber; + + /// `FieldPublisher` types for Publishes field constructs and update the data + type FieldPublisher: FieldPublisher; + /// `ProviderInfo` types for Configuration data for service producers instances type ProviderInfo: ProviderInfo + Send + Clone; @@ -330,6 +337,12 @@ where /// # Type Parameters /// * `T` - The relocatable event data type pub trait SampleMut: DerefMut + Debug +where + T: CommData + Debug, +{ +} + +pub trait EventSampleMut: SampleMut where T: CommData + Debug, { @@ -477,8 +490,8 @@ pub trait Publisher where T: CommData + Debug, { - /// Associated sample type for uninitialized event data - type SampleMaybeUninit<'a>: SampleMaybeUninit + 'a + /// Associated sample type for uninitialized event data. + type SampleMaybeUninit<'a>: SampleMaybeUninit + 'a> where Self: 'a; /// Allocate a buffer slot for the event publication. diff --git a/score/mw/com/rust/score_com_concept/field_concept.rs b/score/mw/com/rust/score_com_concept/field_concept.rs new file mode 100644 index 000000000..1bedb35cc --- /dev/null +++ b/score/mw/com/rust/score_com_concept/field_concept.rs @@ -0,0 +1,145 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// TODOs: +// 1.Get and Set methods for field it is enabled based on tag, do we want to keep same kind of mechanism +// or by default we will enable for user, +// -> we can keep it default enable as of now, +// and later we can add tag based mechanism if required because Interface side we need to check how we can do this +// 2. We are offering get method after subscrption async but before subscription it is sync, +// It is because subscribe API take the consumer instance by value and if we offer async get method then it will create issue with subscription. + +// Note: We are using the event related trait as a base trait for where ever we have same common +// APIs or functionality, as of now there are derived from concept crate but +// we will create a module which will have common trait for event and field which will be used by both event and field as a super trait and +// for this we need to create marker trait for event. + +use crate::*; +use std::fmt::Debug; +use std::future::Future; + +#[allow(dead_code)] +// Temp for build test +// We will remove this once memory layout of same created in rust side like SamplePtr. +#[repr(C)] +#[derive(Debug)] +pub struct MethodReturnTypePtr { + pub value: T, + pub status: Result<()>, +} + +/// FieldSubscriber trait is used to subscribe to a field and get the value of the field. +/// It provides the `get` and `set` methods to get and set the value of the field. +/// It derived from `concept::Subscriber` trait which provides the `subscribe` method to create a field subscription. +/// The `get` and `set` methods for the field instance can be used before subscription. +/// Event related APIs follow the same restriction for concurrent access. +pub trait FieldSubscriber: + concept::Subscriber> +{ + /// Get the current value of the field. + /// + /// #returns + /// Return the result of `MethodReturnTypePtr` which contains the current value of the field. + /// Note: Get Method before subscription is synchronous and after subscription it is asynchronous. + /// It is because subscribe API take the consumer instance by value and if we provide async get method then it will create issue with subscription. + fn get(&self) -> Result>; + + /// Set the value of the field. + /// + /// # Parameters + /// * `value` - The value to set for the field. + /// + /// # Returns + /// Return the result of `MethodReturnTypePtr` which contains the status of the set operation. + /// with the current value of the field. + fn set(&self, value: &T) -> Result>; +} + +/// FieldSubscriber trait is provides the receiving APIs for the field subscription and +/// it is derived from `concept::Subscription` trait which provides the receiving APIs for the field subscription. +/// Additional methods which the field subscription provides are added in this trait. +pub trait FieldSubscription: + concept::Subscription +{ + /// Returns the number of new samples a call to try_receive (given parameter max_num_samples + /// doesn't restrict it) would currently provide. + /// How many new sample available for the user of this field subscription to receive. + fn get_num_new_samples_available(&self) -> Result; + + /// Get the number of samples that can still be received by the user of this field. + /// This is for checking the capacity of the field subscription and to avoid overflow of the field subscription limit. + fn get_free_sample_count(&self) -> Result; + + ///Get the current value of the field. + /// + /// #returns + /// Return the `Future>>` which contains the current value of the field. + fn get(&self) -> impl Future>> + Send; + + ///Set the value of the field. + /// + /// # Parameters + /// * `value` - The value to set for the field. + /// + /// # Returns + /// Return the result of `MethodReturnTypePtr` which contains the status of the set operation. + /// with the current value of the field. + fn set(&self, value: &T) -> Result>; +} + +/// FieldPublisher trait is used to publish a field and update the value of the field. +// Note: We can not use publisher trait from event because that contains the Send Method which is not correct semantic for field. +pub trait FieldPublisher { + type SampleMaybeUninit<'a>: SampleMaybeUninit> + 'a + where + Self: 'a; + + /// Create a new publisher for the specified event source. + fn new(identifier: &str, instance_info: R::ProviderInfo) -> Result + where + Self: Sized; + + /// Get the allocate sample ptr for the field publisher. + fn allocate(&self) -> Result>; + + /// Update the value of the field with the provided value. + /// This is not zero-copy API. + /// + /// # Parameters + /// * `value` - The value to update for the field. + /// + /// # Returns + /// Return the result of `Result<()>` which contains the status of the update operation. + fn update(&self, value: &T) -> Result<()>; + + /// Register a callback function to handle the set operation for the field. + /// It will create new task or thread to handle the set operation callback function, + /// which will be mostly done using thread pool or async task pool, will be decided at the time of implementation. + /// + /// # Parameters + /// * `callback` - The callback function to handle the set operation for the field. + /// + /// # Returns + /// Return the result of `Result<()>` which contains the status of the register operation. + // TODO: Do we need to make callback lifetime 'static or we keep same as field publisher lifetime. + fn register_set_handler<'a>(&self, callback: impl Fn(&T) + Send + 'a) -> Result<()>; +} + +/// FieldSampleMut trait is used to update the value of the field sample for zero-copy API. +pub trait FieldSampleMut: concept::SampleMut +where + T: CommData + Debug, +{ + /// Update the value for zero-copy API. + fn update(self) -> Result<()>; +} diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_macros.rs index 46cae701f..c1592da9e 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -11,6 +11,22 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +/// Type-state marker for uninitialized field state (compile-time tracking). +#[allow(dead_code)] +pub struct Uninit; + +/// Type-state marker for initialized field state (compile-time tracking). +#[allow(dead_code)] +pub struct Init; + +/// Type-state marker for handler not registered (compile-time tracking). +#[allow(dead_code)] +pub struct HandlerNotSet; + +/// Type-state marker for handler registered (compile-time tracking). +#[allow(dead_code)] +pub struct HandlerSet; + /// Main interface macro that generates Consumer, Producer, and OfferedProducer types /// along with all necessary trait implementations. /// @@ -76,6 +92,12 @@ /// "left_tire" and "exhaust" events. /// - `VehicleOfferedProducer` struct that implements `OfferedProducer` trait for offering /// "left_tire" and "exhaust" events. +// TODO: We need to enable the support for mixed types (Event, Method, Field) in the same interface definition. +// Currently, we are supporting only one type of definition in the interface macro. We will add support for mixed types before enabling field and method for user. +// We will update this macro in such a way so it should not cause in backward compatibility issues for existing users. +// Plan is to have only two match arm in the interface macro, and then validate if given struct field value has literal like Event, Method, Field. +// Currently you may see duplicate code for field and event macro but field related macro just added to verify the example application for APIs usage. +// This file will be optimized as mentioned above. #[macro_export] macro_rules! interface { // Default unique ID based on the module path and interface name @@ -114,11 +136,18 @@ macro_rules! interface { ); }; - (interface $id:ident { $($event_name:ident : Field<$event_type:ty>),+$(,)? }) => { - compile_error!( - "Field definitions are not supported in this macro version. \ - Please use Event syntax for defining events." - ); + (interface $id:ident { $($field_name:ident : Field<$field_type:ty>),+$(,)? }) => { + $crate::interface_common!($id); + $crate::interface_consumer!($id, $($field_name, Field<$field_type>),+); + $crate::interface_producer!($id, $($field_name, Field<$field_type>),+); + }; + (interface $id:ident { + Id = $uid:expr, + $($field_name:ident : Field<$field_type:ty>),+ $(,)? + }) => { + $crate::interface_common!($id, $uid); + $crate::interface_consumer!($id, $($field_name, Field<$field_type>),+); + $crate::interface_producer!($id, $($field_name, Field<$field_type>),+); }; } @@ -184,6 +213,31 @@ macro_rules! interface_consumer { } } }; + ($id:ident, $($field_name:ident, Field<$field_type:ty>),+$(,)?) => { + score_com::paste::paste! { + pub struct [<$id Consumer>] { + $( + pub $field_name: R::FieldSubscriber<$field_type>, + )+ + } + + impl score_com::Consumer for [<$id Consumer>] { + fn new(instance_info: R::ConsumerInfo) -> Self { + [<$id Consumer>] { + $( + $field_name: R::FieldSubscriber::new( + stringify!($field_name), + instance_info.clone() + ).expect(&format!( + "Failed to create subscriber for {}", + stringify!($field_name) + )), + )+ + } + } + } + } + }; } /// Macro to implement the Producer and OfferedProducer traits for @@ -250,6 +304,88 @@ macro_rules! interface_producer { } } }; + ($id:ident, $($field_name:ident, Field<$field_type:ty>),+$(,)?) => { + score_com::paste::paste! { + // Producer struct with proc macro validation + #[derive($crate::score_com_concept_macros::TypeStateFieldValidator)] + pub struct [<$id Producer>] { + $( + pub $field_name: R::FieldPublisher<$field_type>, + )+ + pub instance_info: R::ProviderInfo, + } + + pub struct [<$id OfferedProducer>] { + $( + pub $field_name: R::FieldPublisher<$field_type>, + )+ + instance_info: R::ProviderInfo, + } + + // Internal implementation + impl [<$id Producer>] { + /// Internal offer implementation + /// Use init_field().update_*(...).register_set_handler_*(...).offer() instead. + #[doc(hidden)] + fn _offer_internal(self) -> score_com::Result<[<$id OfferedProducer>]> { + // Create OfferedProducer from consumed producer + let offered = [<$id OfferedProducer>] { + $( + $field_name: self.$field_name, + )+ + instance_info: self.instance_info.clone(), + }; + // Offer the service instance to make it discoverable + self.instance_info.offer_service()?; + Ok(offered) + } + } + + // We can not remove the offer method from the Producer trait, but we can override it to panic with a clear message. + // Also adding compiler warning or error for this is not possible, we will rely on documentation and panic. + // if user call this directly, then it will panic and it is against the intended usage of the APIs. + // TODO: Need to think about this more, when we have more complex interface with mixed types. + // Also update the documentation for this, so user should not call offer() directly from Producer struct. + impl score_com::Producer for [<$id Producer>] { + type Interface = [<$id Interface>]; + type OfferedProducer = [<$id OfferedProducer>]; + fn offer(self) -> score_com::Result { + panic!("Cannot offer field-based producer without initializing fields and registering handlers.\n\ + Use: producer.init_field().update_*(...).register_set_handler_*(...).offer()"); + + } + + fn new(instance_info: R::ProviderInfo) -> score_com::Result { + Ok(Self { + $( + $field_name: R::FieldPublisher::new( + stringify!($field_name), + instance_info.clone() + )?, + )+ + instance_info, + }) + } + } + + impl score_com::OfferedProducer + for [<$id OfferedProducer>] { + type Interface = [<$id Interface>]; + type Producer = [<$id Producer>]; + + fn unoffer(self) -> score_com::Result { + let producer = [<$id Producer>] { + $( + $field_name: self.$field_name, + )+ + instance_info: self.instance_info.clone(), + }; + self.instance_info.stop_offer_service()?; + Ok(producer) + } + } + } + }; } mod tests { diff --git a/score/mw/com/rust/score_com_concept/lib.rs b/score/mw/com/rust/score_com_concept/lib.rs index 920c9b7bb..d53e066cb 100644 --- a/score/mw/com/rust/score_com_concept/lib.rs +++ b/score/mw/com/rust/score_com_concept/lib.rs @@ -22,10 +22,12 @@ /// boundaries without violating Rust's ownership rules. mod concept; mod error; +mod field_concept; mod interface_macros; mod reloc; pub use concept::*; pub use error::*; +pub use field_concept::*; #[doc(hidden)] pub use paste; pub use reloc::Reloc; From f971a9a6707749bba27661eea3eebc7b652799e7 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Wed, 29 Jul 2026 12:42:23 +0530 Subject: [PATCH 12/25] Rust::com Runtime placeholder Implementation for Field * Lola Runtime placeholder implementaion for field producer and consumer * Mock Runtime placeholder implementation --- .../rust/com-api/com-api-runtime-lola/BUILD | 7 +- .../com-api-runtime-lola/field_consumer.rs | 129 +++++++++++ .../com-api-runtime-lola/field_producer.rs | 107 +++++++++ .../rust/com-api/com-api-runtime-lola/lib.rs | 4 + .../com-api/com-api-runtime-lola/producer.rs | 12 +- .../com-api/com-api-runtime-lola/runtime.rs | 6 +- .../com-api/com-api-runtime-mock/runtime.rs | 211 +++++++++++++++++- 7 files changed, 457 insertions(+), 19 deletions(-) create mode 100644 score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs create mode 100644 score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD index 465705984..8532f3df2 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD @@ -15,12 +15,7 @@ load("@rules_rust//rust:defs.bzl", "rust_doc_test", "rust_library", "rust_test") rust_library( name = "com-api-runtime-lola", - srcs = [ - "consumer.rs", - "lib.rs", - "producer.rs", - "runtime.rs", - ], + srcs = glob(["**/*.rs"]), edition = "2024", visibility = ["//score/mw/com:__subpackages__"], deps = [ diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs new file mode 100644 index 000000000..81e0a5d77 --- /dev/null +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs @@ -0,0 +1,129 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +//! Field consumer implementation for Lola runtime. +//! It implements the field consumer related traits. +//! We are using the event related trait as a base trait for whereever we have same common +//! APIs or functionality, as of now there are derived from concept crate but +//! we will create a module which will have common trait for event and field which will be used by both event and field consumer/publisher. + +use core::fmt::Debug; +use core::marker::PhantomData; + +use bridge_ffi_rs::FFIBridge; +use score_com_concept::{ + CommData, FieldSubscriber, FieldSubscription, MethodReturnTypePtr, Result, SampleContainer, + Subscriber, Subscription, +}; + +use crate::consumer::LolaSample; +use crate::{LolaConsumerInfo, LolaRuntimeImpl}; + +/// Field subscriber type which implements the FieldSubscriber trait for Lola runtime. +/// It will implement `subscribe` method to create a field subscription and `get` and `set` methods to get and set the value of the field. +pub struct LolaFieldSubscriber { + _data: PhantomData, + _bridge: PhantomData, +} + +/// Marker implementation of FieldSubscriber trait. +impl FieldSubscriber> + for LolaFieldSubscriber +{ + fn get(&self) -> Result> { + todo!() + } + fn set(&self, _value: &T) -> Result> { + todo!() + } +} + +/// Implementation of Subscriber trait which provides `new` and `subscribe` methods for LolaFieldSubscriber. +impl Subscriber> + for LolaFieldSubscriber +{ + type Subscription = LolaFieldSubscription; + + fn new(_identifier: &'static str, _instance_info: LolaConsumerInfo) -> Result { + todo!() + } + + fn subscribe(self, _max_num_samples: usize) -> Result { + todo!() + } +} + +/// FieldSubscription type which provides data receiving APIs and unsubscribe method. +pub struct LolaFieldSubscription { + _data: PhantomData, + _bridge: PhantomData, +} + +impl FieldSubscription> + for LolaFieldSubscription +{ + fn get_free_sample_count(&self) -> Result { + todo!() + } + + fn get_num_new_samples_available(&self) -> Result { + todo!() + } + + fn get(&self) -> impl Future>> + Send { + async { todo!() } + } + fn set(&self, _value: &T) -> Result> { + todo!() + } +} + +/// Implementation of Subscription trait which provides receiving APIs. +impl Subscription> + for LolaFieldSubscription +{ + type Subscriber = LolaFieldSubscriber; + type Sample<'a> + = LolaSample + where + Self: 'a; + + fn unsubscribe(self) -> Self::Subscriber { + todo!() + } + + fn try_receive<'a>( + &'a self, + _scratch: &'_ mut SampleContainer>, + _max_samples: usize, + ) -> Result { + todo!() + } + + fn cancellable_receive<'a>( + &'a self, + _scratch: SampleContainer>, + _new_samples: usize, + _max_samples: usize, + _cancellation: impl core::future::Future + Send + 'static, + ) -> impl core::future::Future>, Result)> + 'a + { + async { todo!() } + } + + fn to_stream<'a>( + &'a mut self, + ) -> impl futures::stream::Stream>> + Unpin + 'a { + futures::stream::empty() + } +} diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs new file mode 100644 index 000000000..14518dd82 --- /dev/null +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs @@ -0,0 +1,107 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +//! Field producer implementation for Lola runtime. + +use core::fmt::Debug; +use core::marker::PhantomData; +use core::todo; + +use bridge_ffi_rs::FFIBridge; +use score_com_concept::{ + CommData, FieldPublisher, FieldSampleMut, Result, SampleMaybeUninit as SampleMaybeUninitTrait, +}; + +use crate::LolaProviderInfo; +use crate::LolaRuntimeImpl; + +pub struct LolaFieldPublisher { + _data: PhantomData, + _bridge: PhantomData, +} + +#[derive(Debug)] +pub struct LolaFieldSampleMut { + _data: PhantomData, +} + +impl core::ops::Deref for LolaFieldSampleMut { + type Target = T; + fn deref(&self) -> &T { + todo!() + } +} +impl core::ops::DerefMut for LolaFieldSampleMut { + fn deref_mut(&mut self) -> &mut T { + todo!() + } +} + +impl score_com_concept::SampleMut for LolaFieldSampleMut {} + +impl FieldSampleMut for LolaFieldSampleMut { + fn update(self) -> Result<()> { + todo!() + } +} + +#[derive(Debug)] +pub struct LolaFieldSampleMaybeUninit<'a, T> { + _data: core::mem::MaybeUninit, + _lt: PhantomData<&'a T>, +} + +impl<'a, T: CommData + Debug> AsMut> + for LolaFieldSampleMaybeUninit<'a, T> +{ + fn as_mut(&mut self) -> &mut core::mem::MaybeUninit { + &mut self._data + } +} +impl<'a, T: CommData + Debug> SampleMaybeUninitTrait for LolaFieldSampleMaybeUninit<'a, T> { + type SampleMut = LolaFieldSampleMut; + unsafe fn assume_init(self) -> LolaFieldSampleMut { + todo!() + } + fn write(self, _value: T) -> LolaFieldSampleMut { + todo!() + } +} + +impl FieldPublisher> + for LolaFieldPublisher +{ + type SampleMaybeUninit<'a> + = LolaFieldSampleMaybeUninit<'a, T> + where + Self: 'a; + + fn new(_identifier: &str, _instance_info: LolaProviderInfo) -> Result { + todo!() + } + fn allocate(&self) -> Result> { + todo!() + } + fn update(&self, _value: &T) -> Result<()> { + todo!() + } + fn register_set_handler<'a>(&self, _callback: impl Fn(&T) + Send + 'a) -> Result<()> { + //If waker get the notification form FFI call then + //Create a task to call the callback with value. + //Thread pool is a option here to run the callback in a separate thread. + //But i feel we still need to think about exection order of that callback, + //Because separate thread can raise concurrency issue / race condition. + + todo!() + } +} diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs index 1f9d80df6..2b034c7ed 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs @@ -26,10 +26,14 @@ //! that utilize the COM API abstractions. mod consumer; +mod field_consumer; +mod field_producer; mod producer; mod runtime; pub use consumer::{LolaConsumerDiscovery, LolaConsumerInfo, LolaSample, LolaSubscribableImpl}; +pub use field_consumer::LolaFieldSubscriber; +pub use field_producer::LolaFieldPublisher; pub use producer::{ LolaProducerBuilder, LolaProviderInfo, LolaPublisher, LolaSampleMaybeUninit, LolaSampleMut, }; diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/producer.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/producer.rs index ad172f54e..6c2211954 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/producer.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/producer.rs @@ -39,9 +39,9 @@ use std::sync::Arc; use score_log as log; use score_com_concept::{ - AllocationFailureReason, Builder, CommData, Error, EventFailedReason, InstanceSpecifier, - Interface, Producer, ProducerBuilder, ProducerFailedReason, ProviderInfo, Publisher, Result, - SampleMaybeUninit, SampleMut, ServiceFailedReason, + AllocationFailureReason, Builder, CommData, Error, EventFailedReason, EventSampleMut, + InstanceSpecifier, Interface, Producer, ProducerBuilder, ProducerFailedReason, ProviderInfo, + Publisher, Result, SampleMaybeUninit, SampleMut, ServiceFailedReason, }; use bridge_ffi_rs::*; @@ -204,7 +204,9 @@ where } } -impl<'a, T, B: FFIBridge> SampleMut for LolaSampleMut<'a, T, B> +impl<'a, T, B: FFIBridge> SampleMut for LolaSampleMut<'a, T, B> where T: CommData + Debug {} + +impl<'a, T, B: FFIBridge> EventSampleMut for LolaSampleMut<'a, T, B> where T: CommData + Debug, { @@ -531,9 +533,9 @@ impl Builder>> mod test { use super::*; use bridge_ffi_mock::{MockFFIBridge, MockPointerAllocator, SharedMockBridge}; - use score_com_concept::{InstanceSpecifier}; use mockall::predicate::*; use mockall::Sequence; + use score_com_concept::InstanceSpecifier; #[derive(Debug, Default)] #[repr(C)] diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs index 7d87da820..5a96e856e 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs @@ -16,8 +16,8 @@ use core::marker::PhantomData; use std::path::{Path, PathBuf}; use crate::{ - LolaConsumerDiscovery, LolaConsumerInfo, LolaProducerBuilder, LolaProviderInfo, LolaPublisher, - LolaSubscribableImpl, + LolaConsumerDiscovery, LolaConsumerInfo, LolaFieldPublisher, LolaFieldSubscriber, + LolaProducerBuilder, LolaProviderInfo, LolaPublisher, LolaSubscribableImpl, }; use score_com_concept::{ Builder, CommData, FindServiceSpecifier, InstanceSpecifier, Interface, Result, Runtime, @@ -36,6 +36,8 @@ impl Runtime for LolaRuntimeImpl { type Subscriber = LolaSubscribableImpl; type ProducerBuilder = LolaProducerBuilder; type Publisher = LolaPublisher; + type FieldPublisher = LolaFieldPublisher; + type FieldSubscriber = LolaFieldSubscriber; type ProviderInfo = LolaProviderInfo; type ConsumerInfo = LolaConsumerInfo; diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs index fceb5b082..bd61dcb38 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs @@ -36,10 +36,12 @@ use std::collections::VecDeque; use std::path::Path; use score_com_concept::{ - Builder, CommData, Consumer, ConsumerBuilder, ConsumerDescriptor, FindServiceSpecifier, - InstanceSpecifier, Interface, Producer, ProducerBuilder, ProviderInfo, Publisher, Result, - Runtime, RuntimeBuilder, Sample, SampleContainer, SampleMaybeUninit, SampleMut, - ServiceDiscovery, Subscriber, Subscription, + Builder, CommData, Consumer, ConsumerBuilder, ConsumerDescriptor, EventSampleMut, + FieldPublisher, FieldSampleMut, FieldSubscriber, FieldSubscription, FindServiceSpecifier, + InstanceSpecifier, Interface, MethodReturnTypePtr, Producer, ProducerBuilder, ProviderInfo, + Publisher, Result, Runtime, RuntimeBuilder, Sample, SampleContainer, + SampleMaybeUninit as SampleMaybeUninitTrait, SampleMaybeUninit, SampleMut, ServiceDiscovery, + Subscriber, Subscription, }; pub struct MockRuntimeImpl {} @@ -69,6 +71,8 @@ impl Runtime for MockRuntimeImpl { type Subscriber = MockSubscribableImpl; type ProducerBuilder = MockProducerBuilder; type Publisher = MockPublisher; + type FieldSubscriber = MockFieldSubscriber; + type FieldPublisher = MockFieldPublisher; type ProviderInfo = MockProviderInfo; type ConsumerInfo = MockConsumerInfo; @@ -191,7 +195,9 @@ where lifetime: PhantomData<&'a T>, } -impl<'a, T> SampleMut for MockSampleMut<'a, T> +impl<'a, T> SampleMut for MockSampleMut<'a, T> where T: CommData + Debug {} + +impl<'a, T> EventSampleMut for MockSampleMut<'a, T> where T: CommData + Debug, { @@ -507,9 +513,202 @@ impl RuntimeBuilderImpl { } } +/// Field subscriber type which implements the FieldSubscriber trait for Mock runtime. +pub struct MockFieldSubscriber { + identifier: &'static str, + instance_info: MockConsumerInfo, + _data: PhantomData, +} + +/// Marker implementation of FieldSubscriber trait. +impl FieldSubscriber for MockFieldSubscriber { + fn get(&self) -> Result> { + todo!() + } + fn set(&self, _value: &T) -> Result> { + todo!() + } +} + +/// Implementation of Subscriber trait for MockFieldSubscriber. +impl Subscriber for MockFieldSubscriber { + type Subscription = MockFieldSubscription; + + fn new(identifier: &'static str, instance_info: MockConsumerInfo) -> Result { + Ok(Self { + identifier, + instance_info, + _data: PhantomData, + }) + } + + fn subscribe(self, _max_num_samples: usize) -> Result { + Ok(MockFieldSubscription { + identifier: self.identifier, + instance_info: self.instance_info, + _data: PhantomData, + }) + } +} + +/// FieldSubscription type which provides data receiving APIs and unsubscribe method. +pub struct MockFieldSubscription { + identifier: &'static str, + instance_info: MockConsumerInfo, + _data: PhantomData, +} + +impl FieldSubscription for MockFieldSubscription { + fn get_free_sample_count(&self) -> Result { + todo!() + } + + fn get_num_new_samples_available(&self) -> Result { + todo!() + } + + fn get(&self) -> impl Future>> + Send { + async { todo!() } + } + fn set(&self, _value: &T) -> Result> { + todo!() + } +} + +/// Implementation of Subscription trait which provides receiving APIs. +impl Subscription for MockFieldSubscription { + type Subscriber = MockFieldSubscriber; + type Sample<'a> + = MockSample<'a, T> + where + Self: 'a; + + fn unsubscribe(self) -> Self::Subscriber { + MockFieldSubscriber { + identifier: self.identifier, + instance_info: self.instance_info, + _data: PhantomData, + } + } + + fn try_receive<'a>( + &'a self, + _scratch: &'_ mut SampleContainer>, + _max_samples: usize, + ) -> Result { + todo!() + } + + fn cancellable_receive<'a>( + &'a self, + _scratch: SampleContainer>, + _new_samples: usize, + _max_samples: usize, + _cancellation: impl Future + Send + 'static, + ) -> impl Future>, Result)> + 'a { + async { todo!() } + } + + fn to_stream<'a>(&'a mut self) -> impl Stream>> + Unpin + 'a { + stream::empty() + } +} + +/// Field publisher type for Mock runtime. +pub struct MockFieldPublisher { + _data: PhantomData, +} + +/// Field sample mutable type. +#[derive(Debug)] +pub struct MockFieldSampleMut<'a, T: CommData + Debug> { + data: T, + _lifetime: PhantomData<&'a T>, +} + +impl<'a, T: CommData + Debug> Deref for MockFieldSampleMut<'a, T> { + type Target = T; + fn deref(&self) -> &T { + &self.data + } +} + +impl<'a, T: CommData + Debug> DerefMut for MockFieldSampleMut<'a, T> { + fn deref_mut(&mut self) -> &mut T { + &mut self.data + } +} + +impl<'a, T: CommData + Debug> SampleMut for MockFieldSampleMut<'a, T> {} + +impl<'a, T: CommData + Debug> FieldSampleMut for MockFieldSampleMut<'a, T> { + fn update(self) -> Result<()> { + todo!() + } +} + +/// Field sample maybe uninit type. +#[derive(Debug)] +pub struct MockFieldSampleMaybeUninit<'a, T: CommData + Debug> { + data: MaybeUninit, + _lifetime: PhantomData<&'a T>, +} + +impl<'a, T: CommData + Debug> AsMut> for MockFieldSampleMaybeUninit<'a, T> { + fn as_mut(&mut self) -> &mut MaybeUninit { + &mut self.data + } +} + +impl<'a, T: CommData + Debug> SampleMaybeUninitTrait for MockFieldSampleMaybeUninit<'a, T> { + type SampleMut = MockFieldSampleMut<'a, T>; + + unsafe fn assume_init(self) -> MockFieldSampleMut<'a, T> { + MockFieldSampleMut { + data: unsafe { self.data.assume_init() }, + _lifetime: PhantomData, + } + } + + fn write(self, value: T) -> MockFieldSampleMut<'a, T> { + MockFieldSampleMut { + data: value, + _lifetime: PhantomData, + } + } +} + +impl FieldPublisher for MockFieldPublisher { + type SampleMaybeUninit<'a> + = MockFieldSampleMaybeUninit<'a, T> + where + Self: 'a; + + fn new(_identifier: &str, _instance_info: MockProviderInfo) -> Result { + Ok(Self { _data: PhantomData }) + } + + fn allocate(&self) -> Result> { + Ok(MockFieldSampleMaybeUninit { + data: MaybeUninit::uninit(), + _lifetime: PhantomData, + }) + } + + fn update(&self, _value: &T) -> Result<()> { + todo!() + } + + fn register_set_handler<'a>(&self, _callback: impl Fn(&T) + Send + 'a) -> Result<()> { + todo!() + } +} + #[cfg(test)] mod test { - use score_com_concept::{Publisher, SampleContainer, SampleMaybeUninit, SampleMut, Subscription}; + use score_com_concept::{ + Publisher, SampleContainer, SampleMaybeUninit, SampleMut, Subscription, + }; #[test] fn receive_stuff() { From 14f825731a7e870f3ffc5c0b1adfe3587438c6e6 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Wed, 29 Jul 2026 13:02:45 +0530 Subject: [PATCH 13/25] Rust::com Create type state macro for field Init * Create proc macro for field init and set handler validation before offer call --- score/mw/com/rust/score_com.rs | 9 +- score/mw/com/rust/score_com_concept/lib.rs | 1 + score/mw/com/rust/score_com_macros/BUILD | 2 +- score/mw/com/rust/score_com_macros/lib.rs | 29 ++ .../score_com_macros/type_state_validator.rs | 278 ++++++++++++++++++ 5 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 score/mw/com/rust/score_com_macros/type_state_validator.rs diff --git a/score/mw/com/rust/score_com.rs b/score/mw/com/rust/score_com.rs index d16ae15b1..acebb6fb3 100644 --- a/score/mw/com/rust/score_com.rs +++ b/score/mw/com/rust/score_com.rs @@ -136,10 +136,11 @@ pub use com_api_runtime_lola::RuntimeBuilderImpl as LolaRuntimeBuilderImpl; pub use score_com_concept::{ interface, interface_common, interface_consumer, interface_producer, Builder, CommData, - Consumer, ConsumerBuilder, ConsumerDescriptor, Error, FindServiceSpecifier, InstanceSpecifier, - Interface, OfferedProducer, PlacementDefault, Producer, ProducerBuilder, ProviderInfo, - Publisher, Reloc, Result, Runtime, RuntimeBuilder, SampleContainer, SampleMaybeUninit, - SampleMut, ServiceDiscovery, Subscriber, Subscription, + Consumer, ConsumerBuilder, ConsumerDescriptor, Error, EventSampleMut as SampleMut, + FieldPublisher, FieldSampleMut, FieldSubscriber, FieldSubscription, FindServiceSpecifier, + InstanceSpecifier, Interface, OfferedProducer, PlacementDefault, Producer, ProducerBuilder, + ProviderInfo, Publisher, Reloc, Result, Runtime, RuntimeBuilder, SampleContainer, + SampleMaybeUninit, ServiceDiscovery, Subscriber, Subscription, }; #[doc(hidden)] diff --git a/score/mw/com/rust/score_com_concept/lib.rs b/score/mw/com/rust/score_com_concept/lib.rs index d53e066cb..b53c19ef2 100644 --- a/score/mw/com/rust/score_com_concept/lib.rs +++ b/score/mw/com/rust/score_com_concept/lib.rs @@ -28,6 +28,7 @@ mod reloc; pub use concept::*; pub use error::*; pub use field_concept::*; +pub use interface_macros::{HandlerNotSet, HandlerSet, Init, Uninit}; #[doc(hidden)] pub use paste; pub use reloc::Reloc; diff --git a/score/mw/com/rust/score_com_macros/BUILD b/score/mw/com/rust/score_com_macros/BUILD index 25ec089ac..167d730b4 100644 --- a/score/mw/com/rust/score_com_macros/BUILD +++ b/score/mw/com/rust/score_com_macros/BUILD @@ -15,7 +15,7 @@ load("@rules_rust//rust:defs.bzl", "rust_doc_test", "rust_proc_macro") rust_proc_macro( name = "score-com-macros", - srcs = ["lib.rs"], + srcs = glob(["**/*.rs"]), crate_name = "score_com_macros", visibility = [ "//score/mw/com:__subpackages__", diff --git a/score/mw/com/rust/score_com_macros/lib.rs b/score/mw/com/rust/score_com_macros/lib.rs index e09f43402..09791ad12 100644 --- a/score/mw/com/rust/score_com_macros/lib.rs +++ b/score/mw/com/rust/score_com_macros/lib.rs @@ -15,6 +15,8 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, parse_quote, Data, DeriveInput, Fields, Generics, Meta, Type}; +mod type_state_validator; + /// Derive macro for the `CommData` trait. /// /// Implements `CommData` for a struct or C-like enum, providing a stable string identity @@ -335,6 +337,33 @@ fn collect_field_types(data: &Data) -> Result, ()> { Ok(out) } +/// Procedural macro to generate compile-time type-state validator for Field-based producers. +/// +/// This macro generates a validator struct with phantom type parameters that track +/// the initialization state of each field at compile time. The `offer()` method is only +/// available when all fields have been initialized, preventing runtime errors. +/// +/// # Usage +/// +/// Apply this macro alongside the `interface!` macro for Field-based interfaces: +/// +/// ```ignore +/// #[derive(TypeStateFieldValidator)] +/// struct VehicleFieldProducer { +/// left_tire: R::FieldPublisher, +/// exhaust: R::FieldPublisher, +/// } +/// ``` +/// +/// Macro will generate a `VehicleFieldProducerValidator` struct with phantom type parameters +/// representing the initialization state of each field and handler. The `offer()` method will only be +/// available when all fields are initialized and all handlers are registered, ensuring compile-time safety. +// TODO: Document tests need to be added for this macro, including successful and failed compilation cases. +#[proc_macro_derive(TypeStateFieldValidator)] +pub fn derive_typestate_field_validator(input: TokenStream) -> TokenStream { + type_state_validator::derive_typestate_field_validator_impl(input) +} + // Use doctest to test failed compilations and successful ones /// ``` diff --git a/score/mw/com/rust/score_com_macros/type_state_validator.rs b/score/mw/com/rust/score_com_macros/type_state_validator.rs new file mode 100644 index 000000000..038c340e8 --- /dev/null +++ b/score/mw/com/rust/score_com_macros/type_state_validator.rs @@ -0,0 +1,278 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +use proc_macro::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{parse_macro_input, Data, DeriveInput, Fields, Type}; + +/// The macro generates a validator struct with phantom type parameters that track +/// both the initial value update and handler registration of each field at compile time. +/// The `offer()` method is only available when all fields have been initialized and +/// all handlers have been registered, preventing runtime errors. +/// +/// It generate the field updatd method with concatenated name like `update_` +/// and register handler method with concatenated name like `register_set_handler_`. +/// e.g. for field `left_tire`, the generated methods will be `update_left_tire` and `register_set_handler_left_tire`. +pub fn derive_typestate_field_validator_impl(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + // Extract runtime generic parameter + let (runtime_param_name, runtime_param_with_bounds) = + if let Some(param) = input.generics.params.first() { + match param { + syn::GenericParam::Type(type_param) => { + let name = &type_param.ident; + (quote! { #name }, quote! { #param }) + } + _ => (quote! { R }, quote! { R: score_com::Runtime + ?Sized }), + } + } else { + (quote! { R }, quote! { R: score_com::Runtime + ?Sized }) + }; + // Currently supporting only struct but in future if require will support enum. + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + _ => { + return syn::Error::new_spanned( + name, + "TypeStateFieldValidator only supports structs with named fields", + ) + .to_compile_error() + .into(); + } + }, + _ => { + return syn::Error::new_spanned(name, "TypeStateFieldValidator only supports structs") + .to_compile_error() + .into(); + } + }; + + // Extract field information - use all fields except instance_info + let field_info: Vec<_> = fields + .iter() + .filter_map(|f| { + let ident = f.ident.as_ref()?; + + // Skip instance_info field + // Note: type name is using here as we have same name in interface_macros + // If that change then this also need to be updated. + // Or we need to find some common solution like const name. + if ident == "instance_info" { + return None; + } + + Some(( + ident, // struct field name + ident, // public field name (same as struct field) for methods generation. + &f.ty, // field type + )) + }) + .collect(); + + if field_info.is_empty() { + return syn::Error::new_spanned( + name, + "No fields found for validation (excluding instance_info)", + ) + .to_compile_error() + .into(); + } + + let struct_field_names: Vec<_> = field_info.iter().map(|(sf, _, _)| sf).collect(); + let public_field_names: Vec<_> = field_info.iter().map(|(_, pf, _)| pf).collect(); + let field_types: Vec<_> = field_info.iter().map(|(_, _, ty)| ty).collect(); + + // Extract inner types from R::FieldPublisher -> T + let inner_types: Vec<_> = field_types + .iter() + .map(|ty| { + // Try to extract T from R::FieldPublisher + if let Type::Path(type_path) = ty { + // Look for the last segment which should be FieldPublisher + if let Some(segment) = type_path.path.segments.last() { + //Note: Same here we are using trait name directly + // But if that change then this also need to be updated. + if segment.ident == "FieldPublisher" { + // Extract the type argument + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { + return inner_ty; + } + } + } + } + } + // Fallback: use the full type + *ty + }) + .collect(); + // Generate the validator struct name - e.g., for VehicleProducer, the validator will be VehicleValidator + let validator_name = syn::Ident::new(&format!("{}Validator", name), name.span()); + + // Generate type parameters for each field's UPDATE state (S0, S1, S2, ...) + let field_update_state_params: Vec<_> = public_field_names + .iter() + .enumerate() + .map(|(i, _)| syn::Ident::new(&format!("S{}", i), proc_macro::Span::call_site().into())) + .collect(); + + // Generate type parameters for each field's HANDLER state (H0, H1, H2, ...) + let field_handler_state_params: Vec<_> = public_field_names + .iter() + .enumerate() + .map(|(i, _)| syn::Ident::new(&format!("H{}", i), proc_macro::Span::call_site().into())) + .collect(); + + // Generate update methods - each one changes its field's UPDATE state from current to Init + // while preserving HANDLER state + let update_methods = public_field_names + .iter() + .zip(struct_field_names.iter()) + .zip(inner_types.iter()) + .enumerate() + .map(|(i, ((pub_name, struct_name), inner_ty))| { + // Generate the method name for updating this field - e.g., update_left_tire for field left_tire + let update_fn = syn::Ident::new(&format!("update_{}", pub_name), pub_name.span()); + + // Build the "after" UPDATE state parameter list where this field is Init + let after_update_states: Vec<_> = field_update_state_params + .iter() + .enumerate() + .map(|(j, param)| { + if i == j { + quote! { ::score_com::Init } + } else { + quote! { #param } + } + }) + .collect(); + + quote! { + impl<#runtime_param_with_bounds, #(#field_update_state_params),*, #(#field_handler_state_params),*> + #validator_name<#runtime_param_name, #(#field_update_state_params),*, #(#field_handler_state_params),*> + { + pub fn #update_fn( + mut self, + value: &#inner_ty + ) -> score_com::Result<#validator_name<#runtime_param_name, #(#after_update_states),*, #(#field_handler_state_params),*>> + { + self.producer.#struct_name.update(value)?; + Ok(#validator_name { + producer: self.producer, + _phantom: core::marker::PhantomData, + }) + } + } + } + }); + + // Generate register_set_handler methods - each one changes its field's HANDLER state + // from HandlerNotSet to HandlerSet while preserving UPDATE state + let register_handler_methods = public_field_names + .iter() + .zip(struct_field_names.iter()) + .zip(inner_types.iter()) + .enumerate() + .map(|(i, ((pub_name, struct_name), inner_ty))| { + let register_fn = syn::Ident::new( + &format!("register_set_handler_{}", pub_name), + pub_name.span(), + ); + + // Build the "after" HANDLER state parameter list where this field is HandlerSet + let after_handler_states: Vec<_> = field_handler_state_params + .iter() + .enumerate() + .map(|(j, param)| { + if i == j { + quote! { ::score_com::HandlerSet } + } else { + quote! { #param } + } + }) + .collect(); + + quote! { + impl<#runtime_param_with_bounds, #(#field_update_state_params),*, #(#field_handler_state_params),*> + #validator_name<#runtime_param_name, #(#field_update_state_params),*, #(#field_handler_state_params),*> + where + <#runtime_param_name as score_com::Runtime>::FieldPublisher<#inner_ty>: Send, + { + pub fn #register_fn(mut self, handler: F) -> score_com::Result<#validator_name<#runtime_param_name, #(#field_update_state_params),*, #(#after_handler_states),*>> + where + F: Fn(&#inner_ty) + Send + 'static, + { + self.producer.#struct_name.register_set_handler(handler)?; + Ok(#validator_name { + producer: self.producer, + _phantom: core::marker::PhantomData, + }) + } + } + } + }); + + // Generate list of all Init states for the offer() impl + let all_init_states = vec![quote! { ::score_com::Init }; field_update_state_params.len()]; + + // Generate list of all HandlerSet states for the offer() impl + let all_handler_set_states = + vec![quote! { ::score_com::HandlerSet }; field_handler_state_params.len()]; + + // Generate list of all Uninit states for the validator() method + let all_uninit_states = vec![quote! { ::score_com::Uninit }; field_update_state_params.len()]; + + // Generate list of all HandlerNotSet states for the validator() method + let all_handler_not_set_states = + vec![quote! { ::score_com::HandlerNotSet }; field_handler_state_params.len()]; + + let expanded = quote! { + // Validator struct with dual type-state tracking: + // - First set of params (S0, S1, ...) track field UPDATE state (Uninit/Init) + // - Second set of params (H0, H1, ...) track HANDLER registration state (HandlerNotSet/HandlerSet) + pub struct #validator_name<#runtime_param_with_bounds, #(#field_update_state_params),*, #(#field_handler_state_params),*> { + producer: #name<#runtime_param_name>, + _phantom: core::marker::PhantomData<(#(#field_update_state_params,)* #(#field_handler_state_params,)*)>, + } + + // Update methods that change UPDATE state types (Uninit -> Init) + #(#update_methods)* + + // Register set handler methods that change HANDLER state types (HandlerNotSet -> HandlerSet) + #(#register_handler_methods)* + + // offer() is only available when ALL fields are Init AND all handlers are HandlerSet + impl<#runtime_param_with_bounds> #validator_name<#runtime_param_name, #(#all_init_states),*, #(#all_handler_set_states),*> { + pub fn offer(self) -> score_com::Result<<#name<#runtime_param_name> as score_com::Producer<#runtime_param_name>>::OfferedProducer> { + // Call internal offer implementation after validating all fields are initialized and handlers registered + self.producer._offer_internal() + } + } + + // init_field() method consumes producer and returns validator with all fields Uninit and all handlers HandlerNotSet + impl<#runtime_param_with_bounds> #name<#runtime_param_name> { + pub fn init_field(self) -> #validator_name<#runtime_param_name, #(#all_uninit_states),*, #(#all_handler_not_set_states),*> { + #validator_name { + producer: self, + _phantom: core::marker::PhantomData, + } + } + } + }; + + TokenStream::from(expanded) +} From 3d76ef97e8a7c88e294f467da0dab06f254df35f Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Wed, 29 Jul 2026 13:22:45 +0530 Subject: [PATCH 14/25] Rust::com Update the example app with Field APIs usage * Updated example file with Field APIs usage --- score/mw/com/example/com-api-example/BUILD | 1 + .../com-api-gen/com_api_gen.rs | 16 ++- .../com-api-example/src/field_consumer.rs | 119 ++++++++++++++++++ .../com-api-example/src/field_producer.rs | 85 +++++++++++++ .../mw/com/example/com-api-example/src/lib.rs | 4 +- score/mw/com/rust/score_com.rs | 7 +- .../score_com_concept/interface_macros.rs | 2 +- score/mw/com/rust/score_com_concept/lib.rs | 1 + 8 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 score/mw/com/example/com-api-example/src/field_consumer.rs create mode 100644 score/mw/com/example/com-api-example/src/field_producer.rs diff --git a/score/mw/com/example/com-api-example/BUILD b/score/mw/com/example/com-api-example/BUILD index 10e025029..523a6a86e 100644 --- a/score/mw/com/example/com-api-example/BUILD +++ b/score/mw/com/example/com-api-example/BUILD @@ -25,6 +25,7 @@ rust_library( "@score_baselibs//src/log/score_log", "@score_communication_crate_index//:clap", "@score_communication_crate_index//:futures", + "@score_communication_crate_index//:tokio", ], ) diff --git a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs index 99eb1550e..3196eb072 100644 --- a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs +++ b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs @@ -11,7 +11,7 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -use score_com::{interface, CommData, ProviderInfo, Publisher, Reloc, Subscriber}; +use score_com::{interface, CommData, FieldPublisher, ProviderInfo, Publisher, Reloc, Subscriber}; use score_log::ScoreDebug; #[derive(Debug, Reloc, CommData, ScoreDebug)] @@ -47,3 +47,17 @@ interface!( exhaust: Event, } ); + +// Field-based interface with compile-time initialization safety. +// All fields must be explicitly initialized via the Type State pattern before offering. +// The Type State pattern ensures that you cannot call offer() until all fields have been updated. +// Just for demonstration of APIs usage we are creating a separate interface for field, +// we have plan to update the interface macro to support mixed event and field interface in future. +// https://github.com/eclipse-score/communication/issues/701 +interface!( + interface VehicleField { + Id = "VehicleFieldInterface", + left_tire: Field, + exhaust: Field, + } +); diff --git a/score/mw/com/example/com-api-example/src/field_consumer.rs b/score/mw/com/example/com-api-example/src/field_consumer.rs new file mode 100644 index 000000000..bc332f6f6 --- /dev/null +++ b/score/mw/com/example/com-api-example/src/field_consumer.rs @@ -0,0 +1,119 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#![allow(unused)] + +use score_com::{ + Builder, FieldSubscriber, FieldSubscription, FindServiceSpecifier, InstanceSpecifier, + Interface, Runtime, SampleContainer, ServiceDiscovery, Subscriber, Subscription, +}; + +use com_api_gen::{Tire, VehicleFieldInterface}; + +type VehicleFieldConsumer = ::Consumer; + +// create the consumer. +fn create_consumer_field( + runtime: &R, + service_id: InstanceSpecifier, +) -> VehicleFieldConsumer { + let consumer_discovery = + runtime.find_service::(FindServiceSpecifier::Specific(service_id)); + let available_service_instances = consumer_discovery + .get_available_instances() + .expect("Failed to get available service instances"); + + // Select service instance at specific handle_index + let handle_index = 0; // or any index you need from vector of instances + let consumer_builder = available_service_instances + .into_iter() + .nth(handle_index) + .expect("Failed to get consumer builder at specified handle index"); + + consumer_builder + .build() + .expect("Failed to build consumer instance") +} + +async fn process_get_method_async(subscription: S) +where + S: FieldSubscription, + R: Runtime, +{ + // Get field value asynchronously + match subscription.get().await { + Ok(_method_return) => { + println!("Current tire pressure from spawned task"); + } + Err(e) => eprintln!("Failed to get tire pressure: {:?}", e), + } + + println!("Async subscription processing in spawned task completed"); +} + +// Function to demonstrate the usage of the consumer to get and set fields, +// Subscribe to the fields event and it provides the set and get method as well. +fn consumer_processing_field(consumer: VehicleFieldConsumer) +where + <::FieldSubscriber as Subscriber>::Subscription: Send + 'static, +{ + // Field consumer API methods + // But they demonstrate the correct API usage pattern + // TODO: Currently we are not offering the get method async in FieldSubscriber + // because async call will may run in different thread and that will cause the issue in subscription. + let _ = consumer + .left_tire + .get() + .map(|result| println!("Got field value via consumer: {:?}", result)); + + let _ = consumer + .left_tire + .set(&Tire { pressure: 30.0 }) + .map(|result| println!("Set field value via consumer: {:?}", result)); + + // Subscribe to the field to receive updates + let subscription = consumer + .left_tire + .subscribe(3) + .expect("Failed to subscribe to field"); + + // Create scope for sample_buf to ensure it's dropped before tokio::spawn + { + let mut sample_buf = SampleContainer::new(3); + + // Poll for updates (non-blocking) + match subscription.try_receive(&mut sample_buf, 1) { + Ok(n) if n > 0 => { + while let Some(sample) = sample_buf.pop_front() { + println!("Updated tire pressure: {:?}", *sample); + } + } + _ => { + println!("No new tire pressure updates available"); + } + } + // sample_buf is dropped here at end of scope + } + + // Set via subscription + let _ = subscription + .set(&Tire { pressure: 35.0 }) + .map(|result| println!("Set field value via subscription: {:?}", result)); + + // Spawn async task with subscription + // The subscription is moved into the task + tokio::spawn(async move { + process_get_method_async(subscription).await; + // subscription is automatically unsubscribed when dropped at end of task + }); +} diff --git a/score/mw/com/example/com-api-example/src/field_producer.rs b/score/mw/com/example/com-api-example/src/field_producer.rs new file mode 100644 index 000000000..e098efc0c --- /dev/null +++ b/score/mw/com/example/com-api-example/src/field_producer.rs @@ -0,0 +1,85 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#![allow(unused)] + +use score_com::{Builder, FieldPublisher, InstanceSpecifier, Interface, Producer, Runtime}; + +use com_api_gen::{Exhaust, Tire, VehicleFieldInterface}; + +// VehicleFieldProducer is the producer type for the VehicleField interface (before offering) +type VehicleFieldProducer = ::Producer; +// VehicleFieldOfferedProducer is the offered producer type for the VehicleField interface (fields support update/set-handler) +type VehicleFieldOfferedProducer = + <::Producer as Producer>::OfferedProducer; + +// Below function just demonstrate the field APIs usage +// This build fine but it can not run because we have not implemented the field APIs in Lola runtime yet. + +// Producer creation and intialization of fields with initial values and set handlers for the fields +// It will return the offered producer instance which can be used to update the fields. +fn create_producer_field( + runtime: &R, + service_id: InstanceSpecifier, + initial_tire_value: Tire, + initial_exhaust_value: Exhaust, +) -> VehicleFieldOfferedProducer +where + ::FieldPublisher: Send + Sync, + ::FieldPublisher: Send, +{ + let producer_builder = runtime.producer_builder::(service_id); + let producer = producer_builder + .build() + .expect("Failed to build producer instance"); + + // Use validator pattern with compile-time type-state validation + // Must register handlers and initialize all fields before offer() is available + let offered = producer + .init_field() + .register_set_handler_left_tire(move |val: &Tire| { + println!("Received tire pressure update: {:?}", val); + // Additional logic to handle the tire pressure update can be added here + // For example, we can increment value or conver unit and update the field again. + // TODO: in working example add that logic to demonstrate the set handler usage. + // Note: I think producer may be need clone ? + }) + .expect("Failed to register set handlers") + .register_set_handler_exhaust(|_val: &Exhaust| { + println!("Received exhaust update"); + }) + .expect("Failed to register set handlers") + .update_left_tire(&initial_tire_value) + .expect("Failed to update left_tire field") + .update_exhaust(&initial_exhaust_value) + .expect("Failed to update exhaust field") + .offer() + .expect("Failed to offer producer instance"); + + offered +} + +// Function to demonstrate the usage of the offered producer to update fields +fn offered_producer_process(offered_producer: VehicleFieldOfferedProducer) { + // Use the offered producer to update fields + let new_tire_value = Tire { pressure: 32.0 }; + let new_exhaust_value = Exhaust {}; + offered_producer + .left_tire + .update(&new_tire_value) + .expect("Failed to update left_tire field"); + offered_producer + .exhaust + .update(&new_exhaust_value) + .expect("Failed to update exhaust field"); +} diff --git a/score/mw/com/example/com-api-example/src/lib.rs b/score/mw/com/example/com-api-example/src/lib.rs index fb4e37ff7..addc5cbcd 100644 --- a/score/mw/com/example/com-api-example/src/lib.rs +++ b/score/mw/com/example/com-api-example/src/lib.rs @@ -12,12 +12,14 @@ ********************************************************************************/ pub mod consumer; +mod field_consumer; +mod field_producer; pub mod producer; pub use consumer::VehicleMonitorConsumer; pub use producer::VehicleMonitorProducer; -use score_com::{Interface, Producer}; use com_api_gen::VehicleInterface; +use score_com::{Interface, Producer}; // Type aliases for generated consumer and offered producer types for the Vehicle interface // VehicleConsumer is the consumer type generated for the Vehicle interface, parameterized by the runtime R diff --git a/score/mw/com/rust/score_com.rs b/score/mw/com/rust/score_com.rs index acebb6fb3..62d47fb58 100644 --- a/score/mw/com/rust/score_com.rs +++ b/score/mw/com/rust/score_com.rs @@ -138,9 +138,10 @@ pub use score_com_concept::{ interface, interface_common, interface_consumer, interface_producer, Builder, CommData, Consumer, ConsumerBuilder, ConsumerDescriptor, Error, EventSampleMut as SampleMut, FieldPublisher, FieldSampleMut, FieldSubscriber, FieldSubscription, FindServiceSpecifier, - InstanceSpecifier, Interface, OfferedProducer, PlacementDefault, Producer, ProducerBuilder, - ProviderInfo, Publisher, Reloc, Result, Runtime, RuntimeBuilder, SampleContainer, - SampleMaybeUninit, ServiceDiscovery, Subscriber, Subscription, + HandlerNotSet, HandlerSet, Init, InstanceSpecifier, Interface, OfferedProducer, + PlacementDefault, Producer, ProducerBuilder, ProviderInfo, Publisher, Reloc, Result, Runtime, + RuntimeBuilder, SampleContainer, SampleMaybeUninit, ServiceDiscovery, Subscriber, Subscription, + Uninit, }; #[doc(hidden)] diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_macros.rs index c1592da9e..9c2e05e7f 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -307,7 +307,7 @@ macro_rules! interface_producer { ($id:ident, $($field_name:ident, Field<$field_type:ty>),+$(,)?) => { score_com::paste::paste! { // Producer struct with proc macro validation - #[derive($crate::score_com_concept_macros::TypeStateFieldValidator)] + #[derive($crate::score_com_macros::TypeStateFieldValidator)] pub struct [<$id Producer>] { $( pub $field_name: R::FieldPublisher<$field_type>, diff --git a/score/mw/com/rust/score_com_concept/lib.rs b/score/mw/com/rust/score_com_concept/lib.rs index b53c19ef2..682a4a7a8 100644 --- a/score/mw/com/rust/score_com_concept/lib.rs +++ b/score/mw/com/rust/score_com_concept/lib.rs @@ -32,3 +32,4 @@ pub use interface_macros::{HandlerNotSet, HandlerSet, Init, Uninit}; #[doc(hidden)] pub use paste; pub use reloc::Reloc; +pub use score_com_macros; From 8e2665361985e0ed70b17834a424d0b8408f9c01 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Wed, 29 Jul 2026 14:10:13 +0530 Subject: [PATCH 15/25] Rust::com Interface and type state macro optimization * Method and field both code generation added on macros --- score/mw/com/example/com-api-example/BUILD | 2 +- .../com-api-gen/com_api_gen.rs | 15 + .../com-api-example/src/field_producer.rs | 4 +- .../rust/com-api/com-api-runtime-lola/BUILD | 2 +- score/mw/com/rust/score_com_concept/BUILD | 3 + .../score_com_concept/interface_macros.rs | 654 ++++++++++++++---- score/mw/com/rust/score_com_macros/lib.rs | 61 +- .../score_com_macros/type_state_validator.rs | 379 ++++++---- 8 files changed, 846 insertions(+), 274 deletions(-) diff --git a/score/mw/com/example/com-api-example/BUILD b/score/mw/com/example/com-api-example/BUILD index 523a6a86e..ac8ae26d0 100644 --- a/score/mw/com/example/com-api-example/BUILD +++ b/score/mw/com/example/com-api-example/BUILD @@ -25,7 +25,7 @@ rust_library( "@score_baselibs//src/log/score_log", "@score_communication_crate_index//:clap", "@score_communication_crate_index//:futures", - "@score_communication_crate_index//:tokio", + "@score_communication_crate_index//:tokio", ], ) diff --git a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs index 3196eb072..14ef74b8c 100644 --- a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs +++ b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs @@ -61,3 +61,18 @@ interface!( exhaust: Field, } ); + +// We can also define mix of event , field and method in one interface. +// TODO : Remove the comment once method design PR is merged. +// interface!( +// interface VehicleMonitor { +// Id = "VehicleMonitorInterface", +// left_tire: Event, +// exhaust: Event, +// left_tire_field: Field, +// exhaust_field: Field, +// update_tire_pressure(Tire) -> (), +// update_front_tires_pressure(Tire, Tire) -> (), +// get_tire_pressure() -> Tire, +// } +// ); diff --git a/score/mw/com/example/com-api-example/src/field_producer.rs b/score/mw/com/example/com-api-example/src/field_producer.rs index e098efc0c..7b60d50fb 100644 --- a/score/mw/com/example/com-api-example/src/field_producer.rs +++ b/score/mw/com/example/com-api-example/src/field_producer.rs @@ -46,7 +46,7 @@ where // Use validator pattern with compile-time type-state validation // Must register handlers and initialize all fields before offer() is available let offered = producer - .init_field() + .init() .register_set_handler_left_tire(move |val: &Tire| { println!("Received tire pressure update: {:?}", val); // Additional logic to handle the tire pressure update can be added here @@ -54,11 +54,9 @@ where // TODO: in working example add that logic to demonstrate the set handler usage. // Note: I think producer may be need clone ? }) - .expect("Failed to register set handlers") .register_set_handler_exhaust(|_val: &Exhaust| { println!("Received exhaust update"); }) - .expect("Failed to register set handlers") .update_left_tire(&initial_tire_value) .expect("Failed to update left_tire field") .update_exhaust(&initial_exhaust_value) diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD index 8532f3df2..5685dcb48 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/BUILD @@ -15,7 +15,7 @@ load("@rules_rust//rust:defs.bzl", "rust_doc_test", "rust_library", "rust_test") rust_library( name = "com-api-runtime-lola", - srcs = glob(["**/*.rs"]), + srcs = glob(["**/*.rs"]), edition = "2024", visibility = ["//score/mw/com:__subpackages__"], deps = [ diff --git a/score/mw/com/rust/score_com_concept/BUILD b/score/mw/com/rust/score_com_concept/BUILD index 2f557d48e..70443f8cf 100644 --- a/score/mw/com/rust/score_com_concept/BUILD +++ b/score/mw/com/rust/score_com_concept/BUILD @@ -57,5 +57,8 @@ rust_unit_test( name = "score_com_concept-macros-unit-tests", srcs = ["interface_macros.rs"], features = ["link_std_cpp_lib"], + # TODO: uncomment this once field or method one PR is merged, + # Unit test failed because macro has field and method both types + tags = ["manual"], deps = ["//score/mw/com/rust:score_com"], ) diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_macros.rs index 9c2e05e7f..deb55c6ce 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -10,46 +10,61 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ - -/// Type-state marker for uninitialized field state (compile-time tracking). +/// Type-state marker for uninitialized field value state (compile-time tracking). +/// +/// These marker types are never constructed as values - they only appear as generic +/// type parameters inside `PhantomData<(S, H)>` on the generated `{Id}Validator` struct +/// (see `TypeStateValidator` in `score_com_macros`). The compiler's `dead_code` lint +/// flags unit structs that are never instantiated, so it is suppressed here deliberately. #[allow(dead_code)] pub struct Uninit; -/// Type-state marker for initialized field state (compile-time tracking). +/// Type-state marker for initialized field value state (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. #[allow(dead_code)] pub struct Init; /// Type-state marker for handler not registered (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. #[allow(dead_code)] pub struct HandlerNotSet; /// Type-state marker for handler registered (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. #[allow(dead_code)] pub struct HandlerSet; /// Main interface macro that generates Consumer, Producer, and OfferedProducer types /// along with all necessary trait implementations. /// +/// Supports Event-only interfaces (backward compatible) and mixed interfaces containing +/// any combination of `Event`, `Field`, and `method_name(Args) -> Return` members +/// in the same definition block. +/// /// Automatically generates unique type names from the identifier of macro invocation. /// For an interface with identifier `{id}`, it generates: /// - `{id}Interface` - Struct representing the interface with INTERFACE_ID constant -/// - `{id}Consumer` - Consumer implementation with event subscribers +/// - `{id}Consumer` - Consumer implementation with event subscribers, field subscribers, +/// and method callers /// - `{id}Producer` - Producer implementation -/// - `{id}OfferedProducer` - Offered producer implementation with event publishers +/// - `{id}OfferedProducer` - Offered producer implementation with event publishers, +/// field publishers, and method handlers /// - Implements the `Interface`, `Consumer`, `Producer`, and `OfferedProducer` traits /// for the respective types. /// - `Interface_ID` is generated by default as the module path + interface name, /// but can be overridden by providing a custom UID as a second parameter to the macro. /// -/// Parameters: -/// - Keywords: `interface` followed by the interface identifier and a block of event definitions. -/// - `$id`: Simple identifier used for type name generation (e.g., Vehicle, Engine) -/// - `$event_name`: Event field name -/// - `$event_type`: Event data type +/// # Member types +/// - `name: Event` - event subscriber / publisher pair +/// - `name: Field` - field subscriber / publisher pair (with set-handler callback support) +/// - `name(Args) -> Return` - method caller / handler pair (fn-like syntax) /// -/// Example usage: +/// # Parameters +/// - Keywords: `interface` followed by the interface identifier and a block of member definitions. +/// - `$id`: Simple identifier used for type name generation (e.g., Vehicle, Engine) +/// - Members can be any mix of `Event`, `Field`, and `name(Args) -> Return` /// -/// With default UID generation (module path + interface name): +/// # Example: Event-only with auto-generated ID /// ```ignore /// mod abc { /// use score_com::interface; @@ -69,8 +84,9 @@ pub struct HandlerSet; /// "left_tire" and "exhaust" events. /// - `VehicleOfferedProducer` struct that implements `OfferedProducer` trait for offering /// "left_tire" and "exhaust" events. +/// - `VehicleConsumer`, `VehicleProducer`, `VehicleOfferedProducer` /// -/// With custom UID: +/// # Example: Mixed interface (Event + Field + Method) with custom ID /// ```ignore /// mod abc { /// use score_com::interface; @@ -78,7 +94,8 @@ pub struct HandlerSet; /// interface Vehicle { /// Id = "AbcInterface", /// left_tire: Event, -/// exhaust: Event, +/// left_tire_field: Field, +/// left_tire_method(Tire) -> Tire, /// } /// ); /// } @@ -86,18 +103,35 @@ pub struct HandlerSet; /// Here Id is explicitly set to "AbcInterface" instead of the default "abc::Vehicle". /// The generated code will include: /// - `VehicleInterface` struct with `INTERFACE_ID = "AbcInterface"` -/// - `VehicleConsumer` struct that implements `Consumer` trait for subscribing -/// to "left_tire" and "exhaust" events. -/// - `VehicleProducer` struct that implements `Producer` trait for producing -/// "left_tire" and "exhaust" events. -/// - `VehicleOfferedProducer` struct that implements `OfferedProducer` trait for offering -/// "left_tire" and "exhaust" events. -// TODO: We need to enable the support for mixed types (Event, Method, Field) in the same interface definition. -// Currently, we are supporting only one type of definition in the interface macro. We will add support for mixed types before enabling field and method for user. -// We will update this macro in such a way so it should not cause in backward compatibility issues for existing users. -// Plan is to have only two match arm in the interface macro, and then validate if given struct field value has literal like Event, Method, Field. -// Currently you may see duplicate code for field and event macro but field related macro just added to verify the example application for APIs usage. -// This file will be optimized as mentioned above. +/// - `VehicleConsumer` with `left_tire: Subscriber`, `left_tire_field: FieldSubscriber`, +/// `left_tire_method: MethodCaller<(Tire,), Tire>` and a convenience `left_tire_method(arg0: Tire)` method. +/// - `VehicleProducer` (derives `TypeStateValidator`) with `left_tire_field: FieldPublisher`, +/// `left_tire_method: MethodHandler<(Tire,), Tire>`. Requires `.init()` chain before `.offer()`. +/// - `VehicleOfferedProducer` with event publisher `left_tire`, plus moved field publisher and +/// method handler. +/// - For `left_tire_field`, the user needs to both update the initial value and register the +/// set-handler callback, using the same `init()` chain, before offering the producer instance. +/// +/// The code will look like this: +/// ```ignore +/// let producer = producer_builder.build().expect("Failed to build producer instance"); +/// producer.init() +/// .update_left_tire_field(&initial_value)? +/// .register_set_handler_left_tire_field(|value| { +/// println!("Received left_tire_field update: {:?}", value); +/// }) +/// .register_left_tire_method_handler(|tire: Tire| { +/// println!("Received left_tire_method call with tire: {:?}", tire); +/// tire +/// }) +/// .offer()?; +/// ``` +/// In the code above, if the user forgets to register the field set-handler or the method +/// handler, it will be a compile-time error, since `init()` requires all handlers to be +/// registered before `offer()` becomes available. +/// +/// If the user calls `producer.offer()` directly (without going through `init()`), it will +/// panic at runtime, since the handlers have not been registered yet. #[macro_export] macro_rules! interface { // Default unique ID based on the module path and interface name @@ -124,30 +158,149 @@ macro_rules! interface { }) => { $crate::interface! { interface $id { - Id = $uid, - $($event_name : Event<$event_type>),+ - }} + Id = $uid, + $($event_name : Event<$event_type>),+ + } + } }; - (interface $id:ident { $($event_name:ident : Method<$event_type:ty>),+$(,)? }) => { - compile_error!( - "Method definitions are not supported in this macro version. \ - Please use Event syntax for defining events." + // Mixed / unified: custom ID + (interface $id:ident { + Id = $uid:expr, + $($members:tt)* + }) => { + $crate::interface_common!($id, $uid); + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[] + @fi[] + @me[] + $($members)* ); }; - (interface $id:ident { $($field_name:ident : Field<$field_type:ty>),+$(,)? }) => { + // Mixed / unified: auto-generated ID + (interface $id:ident { $($members:tt)* }) => { $crate::interface_common!($id); - $crate::interface_consumer!($id, $($field_name, Field<$field_type>),+); - $crate::interface_producer!($id, $($field_name, Field<$field_type>),+); + $crate::_interface_collect_members!( + @id[$id, concat!(module_path!(), "::", stringify!($id))] + @ev[] + @fi[] + @me[] + $($members)* + ); }; - (interface $id:ident { - Id = $uid:expr, - $($field_name:ident : Field<$field_type:ty>),+ $(,)? - }) => { - $crate::interface_common!($id, $uid); - $crate::interface_consumer!($id, $($field_name, Field<$field_type>),+); - $crate::interface_producer!($id, $($field_name, Field<$field_type>),+); +} + +/// Internal recursive-macro helper for `interface!`. +/// +/// Accumulates members into three typed lists, then calls the mixed generator macros. +#[doc(hidden)] +#[macro_export] +macro_rules! _interface_collect_members { + // Base case: nothing left - emit the mixed consumer and producer + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $(,)? + ) => { + $crate::interface_consumer_mixed!( + $id, + events[$($ev_name : $ev_type ,)*], + fields[$($fi_name : $fi_type ,)*], + methods[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + ); + $crate::interface_producer_mixed!( + $id, + events[$($ev_name : $ev_type ,)*], + fields[$($fi_name : $fi_type ,)*], + methods[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + ); + }; + + // Event member: `name : Event ,?` + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $name:ident : Event<$t:ty> + $(, $($rest:tt)*)? + ) => { + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)* $name : $t ,] + @fi[$($fi_name : $fi_type ,)*] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + $($($rest)*)? + ); + }; + + // Field member: `name : Field ,?` + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $name:ident : Field<$t:ty> + $(, $($rest:tt)*)? + ) => { + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)*] + @fi[$($fi_name : $fi_type ,)* $name : $t ,] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + $($($rest)*)? + ); + }; + + // Method member (fn-like syntax): `name(Arg0, Arg1, ...) -> Ret ,?` + // Positional types - no tuple wrapper needed at the user level. + // Internally stored as a bracketed list: name [Arg0, Arg1, ...] -> Ret + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $name:ident ( $($arg_ty:ty),* ) -> $ret:ty + $(, $($rest:tt)*)? + ) => { + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)*] + @fi[$($fi_name : $fi_type ,)*] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)* $name [$($arg_ty),*] -> $ret ,] + $($($rest)*)? + ); + }; + + // Catch-all: unrecognized member - emit a clear compile-time error. + ( + @id[$_id:ident, $_uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $($unknown:tt)+ + ) => { + compile_error!(concat!( + "interface!: unrecognized member syntax: `", + stringify!($($unknown)+), + "`.\n", + "Supported member types:\n", + " name: Event - event subscriber / publisher pair\n", + " name: Field - field subscriber / publisher pair\n", + " name(Arg0, Arg1, ...) -> Ret - method caller / handler pair\n", + "Example:\n", + " interface!(interface MyIface {\n", + " my_event: Event,\n", + " my_field: Field,\n", + " my_method(MyData) -> MyData,\n", + " my_void_method(MyData) -> (),\n", + " my_no_arg_method() -> MyData,\n", + " });" + )); }; } @@ -186,6 +339,7 @@ macro_rules! interface_common { /// Macro to implement the Consumer trait for a given interface ID and its events. /// /// Generates the Consumer struct with subscribers for each event. +// TODO: This can be removed once verification is done that the new interface_producer_mixed! macro works for event-only interfaces. #[macro_export] macro_rules! interface_consumer { ($id:ident, $($event_name:ident, Event<$event_type:ty>),+$(,)?) => { @@ -213,36 +367,13 @@ macro_rules! interface_consumer { } } }; - ($id:ident, $($field_name:ident, Field<$field_type:ty>),+$(,)?) => { - score_com::paste::paste! { - pub struct [<$id Consumer>] { - $( - pub $field_name: R::FieldSubscriber<$field_type>, - )+ - } - - impl score_com::Consumer for [<$id Consumer>] { - fn new(instance_info: R::ConsumerInfo) -> Self { - [<$id Consumer>] { - $( - $field_name: R::FieldSubscriber::new( - stringify!($field_name), - instance_info.clone() - ).expect(&format!( - "Failed to create subscriber for {}", - stringify!($field_name) - )), - )+ - } - } - } - } - }; } +/// This is Event specific. /// Macro to implement the Producer and OfferedProducer traits for /// a given interface ID and its events. /// Generates Producer and OfferedProducer structs with publishers for each event. +// TODO: This can be removed once verification is done that the new interface_producer_mixed! macro works for event-only interfaces. #[macro_export] macro_rules! interface_producer { ($id:ident, $($event_name:ident, Event<$event_type:ty>),+$(,)?) => { @@ -304,38 +435,186 @@ macro_rules! interface_producer { } } }; - ($id:ident, $($field_name:ident, Field<$field_type:ty>),+$(,)?) => { +} + +/// Generates the `{id}Consumer` struct and its `Consumer` trait implementation for +/// interfaces that may contain any combination of events, fields, and methods. +/// +/// # Generated struct fields +/// - `pub $ev_name: R::Subscriber<$ev_type>` - one per event +/// - `pub $fi_name: R::FieldSubscriber<$fi_type>` - one per field +/// - `pub $me_name: R::MethodCaller<($me_arg_ty,...), $me_ret>` - one per method +/// +/// # method wrappers +/// For each method member a positional-argument `pub fn $me_name(&self, arg0: A0, ...)` wrapper +/// is generated (via `_gen_method_wrapper!`). The wrapper packs the positional args into a tuple +/// and dispatches through `MethodCallInput`, so both copy and zero-copy paths use the same call site. +/// The wrapper returns `impl Future> + '_`. +/// copy: `consumer.method(val).await` - `val: T` - copy path +/// zero-copy: `consumer.method(ptr).await` - `ptr: MethodInArgPtr` - zero-copy path +#[doc(hidden)] +#[macro_export] +macro_rules! interface_consumer_mixed { + ( + $id:ident, + events[$($ev_name:ident : $ev_type:ty ,)*], + fields[$($fi_name:ident : $fi_type:ty ,)*], + methods[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + ) => { + score_com::paste::paste! { + pub struct [<$id Consumer>] { + $( + pub $ev_name: R::Subscriber<$ev_type>, + )* + $( + pub $fi_name: R::FieldSubscriber<$fi_type>, + )* + $( + pub $me_name: R::MethodCaller<($($me_arg_ty,)*), $me_ret>, + )* + } + + impl score_com::Consumer for [<$id Consumer>] { + fn new(instance_info: R::ConsumerInfo) -> Self { + [<$id Consumer>] { + $( + $ev_name: R::Subscriber::new( + stringify!($ev_name), + instance_info.clone() + ).expect(&format!( + "Failed to create subscriber for {}", + stringify!($ev_name) + )), + )* + $( + $fi_name: R::FieldSubscriber::new( + stringify!($fi_name), + instance_info.clone() + ).expect(&format!( + "Failed to create field subscriber for {}", + stringify!($fi_name) + )), + )* + $( + $me_name: + as score_com::MethodCaller<($($me_arg_ty,)*), $me_ret, R>>::new( + stringify!($me_name), + instance_info.clone() + ).expect(&format!( + "Failed to create method caller for {}", + stringify!($me_name) + )), + )* + } + } + } + + // Positional-argument convenience wrappers - one per method member. + // The wrapper packs args into a tuple and dispatches via MethodCallInput, + // so copy and zero-copy paths share the same call site. + // copy: consumer.method_name(val).await + // zero-copy: consumer.method_name(ptr).await + impl [<$id Consumer>] { + $( + $crate::_gen_method_wrapper!($me_name ($($me_arg_ty),*) -> $me_ret); + )* + } + } + }; +} + +/// Generates `{id}Producer`, `{id}OfferedProducer`, and all trait implementations for +/// interfaces that may contain any combination of events, fields, and methods. +/// +/// # Design +/// - Event publishers (`R::Publisher`) are created *lazily during `_offer_internal()`* +/// so they are only present on the `OfferedProducer`. +/// - Field publishers (`R::FieldPublisher`) are created eagerly in `Producer::new()` and +/// moved into `OfferedProducer` when the service is offered. +/// - Method handlers (`R::MethodHandler`) likewise created eagerly and moved. +/// +/// When the interface has at least one field or method member, the `Producer` struct derives +/// `TypeStateValidator` which generates the `.init()` entry point and the `update_*` / +/// `register_set_handler_*` / `register_*_handler` chain required before `offer()`. +/// +/// When the interface has only events (no fields, no methods), a plain `offer()` is generated +/// directly (matching the existing event-only pattern). +#[doc(hidden)] +#[macro_export] +macro_rules! interface_producer_mixed { + // Event-only specialisation (no fields, no methods): + // plain offer() without type-state validation - identical to interface_producer! + ( + $id:ident, + events[$($ev_name:ident : $ev_type:ty ,)+], + fields[], + methods[] + ) => { + $crate::interface_producer!($id, $($ev_name, Event<$ev_type>),+); + }; + + // General case: at least one field or method (or both), possibly with events too. + ( + $id:ident, + events[$($ev_name:ident : $ev_type:ty ,)*], + fields[$($fi_name:ident : $fi_type:ty ,)*], + methods[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + ) => { score_com::paste::paste! { - // Producer struct with proc macro validation - #[derive($crate::score_com_macros::TypeStateFieldValidator)] + // Producer struct - derives TypeStateValidator for compile-time offer() gating. + // Fields: FieldPublisher per field + MethodHandler per method. + // Event publishers are NOT stored here; they are created during _offer_internal(). + #[derive($crate::score_com_macros::TypeStateValidator)] pub struct [<$id Producer>] { $( - pub $field_name: R::FieldPublisher<$field_type>, - )+ + $fi_name: R::FieldPublisher<$fi_type>, + )* + $( + $me_name: R::MethodHandler<($($me_arg_ty,)*), $me_ret>, + )* pub instance_info: R::ProviderInfo, } + // OfferedProducer struct - contains event publishers (created on offer), + // plus the moved field publishers and method handlers from Producer. pub struct [<$id OfferedProducer>] { $( - pub $field_name: R::FieldPublisher<$field_type>, - )+ + pub $ev_name: R::Publisher<$ev_type>, + )* + $( + pub $fi_name: R::FieldPublisher<$fi_type>, + )* + $( + $me_name: R::MethodHandler<($($me_arg_ty,)*), $me_ret>, + )* instance_info: R::ProviderInfo, } - // Internal implementation + // Internal implementation - called by the TypeStateValidator's offer() after all + // states have been validated at compile time. impl [<$id Producer>] { - /// Internal offer implementation - /// Use init_field().update_*(...).register_set_handler_*(...).offer() instead. #[doc(hidden)] - fn _offer_internal(self) -> score_com::Result<[<$id OfferedProducer>]> { - // Create OfferedProducer from consumed producer + pub fn _offer_internal( + self, + ) -> score_com::Result<[<$id OfferedProducer>]> { let offered = [<$id OfferedProducer>] { $( - $field_name: self.$field_name, - )+ + $ev_name: R::Publisher::new( + stringify!($ev_name), + self.instance_info.clone() + ).expect(&format!( + "Failed to create publisher for {}", + stringify!($ev_name) + )), + )* + $( + $fi_name: self.$fi_name, + )* + $( + $me_name: self.$me_name, + )* instance_info: self.instance_info.clone(), }; - // Offer the service instance to make it discoverable self.instance_info.offer_service()?; Ok(offered) } @@ -349,45 +628,162 @@ macro_rules! interface_producer { impl score_com::Producer for [<$id Producer>] { type Interface = [<$id Interface>]; type OfferedProducer = [<$id OfferedProducer>]; - fn offer(self) -> score_com::Result { - panic!("Cannot offer field-based producer without initializing fields and registering handlers.\n\ - Use: producer.init_field().update_*(...).register_set_handler_*(...).offer()"); + fn offer(self) -> score_com::Result { + panic!( + "ERROR: Cannot call {producer}.offer() directly.\n\ + All fields must be initialized and all handlers must be registered first.\n\ + Correct usage: producer.init()\ + .update_(&val)?\ + .register_set_handler_(|v| {{ ... }})\ + .register__handler(|args| {{ ... }})\ + .offer()?", + producer = stringify!([<$id Producer>]) + ) } fn new(instance_info: R::ProviderInfo) -> score_com::Result { - Ok(Self { + Ok([<$id Producer>] { $( - $field_name: R::FieldPublisher::new( - stringify!($field_name), + $fi_name: R::FieldPublisher::new( + stringify!($fi_name), instance_info.clone() )?, - )+ + )* + $( + $me_name: + as score_com::MethodHandler<($($me_arg_ty,)*), $me_ret, R>>::new( + stringify!($me_name), + instance_info.clone() + )?, + )* instance_info, }) } } + // OfferedProducer trait impl - unoffer() stops the service and returns the Producer. impl score_com::OfferedProducer - for [<$id OfferedProducer>] { + for [<$id OfferedProducer>] + { type Interface = [<$id Interface>]; type Producer = [<$id Producer>]; fn unoffer(self) -> score_com::Result { - let producer = [<$id Producer>] { - $( - $field_name: self.$field_name, - )+ - instance_info: self.instance_info.clone(), - }; self.instance_info.stop_offer_service()?; - Ok(producer) + Ok([<$id Producer>] { + $( + $fi_name: self.$fi_name, + )* + $( + $me_name: self.$me_name, + )* + instance_info: self.instance_info, + }) } } } }; } +/// Entry-point wrapper generator. +/// Every generated wrapper returns `impl Future>> + '_`. +/// +/// # Generated call sites +/// ```text +/// consumer.method(val).await - copy path - val: ArgType +/// consumer.method(ptr).await - zero-copy - ptr: MethodInArgPtr +/// ``` +#[doc(hidden)] +#[macro_export] +macro_rules! _gen_method_wrapper { + // 0 args - invoke_with_copy directly; no zero-copy path (nothing to allocate). + // This is for kind of `get` methods that take no arguments and return a value. + ($me_name:ident () -> $me_ret:ty) => { + pub fn $me_name<'a>(&'a self) -> impl core::future::Future::MethodReturnSample<$me_ret>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.$me_name, ()) + } + }; + // 1–N args - delegate to the self-counting recursive macro. + ($me_name:ident ($($t:ty),+) -> $me_ret:ty) => { + $crate::_gen_method_wrapper_collect!( + $me_name -> $me_ret ; + @counter[] + @acc[] + @types[$($t),+] + ); + }; +} + +/// Recursive macro for `_gen_method_wrapper!`. +/// +/// Self-counting: instead of zipping the method's positional type list against a +/// pre-defined pool of `(arg_name, generic_name)` identifiers, this recursive macro synthesizes +/// a fresh, unique `(argN : _AN : TypeN)` triplet at each recursion step directly from a +/// growing counter of `n` marker tokens (via `paste!`), then calls +/// `_gen_method_wrapper_body!` once the type list is exhausted. +/// +/// This mirrors the self-contained recursion used by `impl_all_arities!` in +/// `method_arities.rs`: there is no separate pool to keep in sync, and no fixed +/// argument-count limit - any arity supported by `method_arities.rs` works automatically. +#[doc(hidden)] +#[macro_export] +macro_rules! _gen_method_wrapper_collect { + // Base: all types consumed - emit the function via the body macro. + ( + $me_name:ident -> $me_ret:ty ; + @counter[$($n:tt)*] + @acc[$($acc:tt),*] + @types[] + ) => { + $crate::_gen_method_wrapper_body!($me_name -> $me_ret ; [$($acc),*]); + }; + + // Step: consume one type, grow the counter by one `n`, and synthesize a fresh + // (param, generic) identifier pair from the counter via `paste!`. + ( + $me_name:ident -> $me_ret:ty ; + @counter[$($n:tt)*] + @acc[$($acc:tt),*] + @types[$t:ty $(, $rest_t:ty)*] + ) => { + score_com::paste::paste! { + $crate::_gen_method_wrapper_collect!( + $me_name -> $me_ret ; + @counter[$($n)* n] + @acc[$($acc,)* ([] : [<_A $($n)*>] : $t)] + @types[$($rest_t),*] + ); + } + }; +} + +/// Generates the wrapper function from an accumulated list of `(argN : _AN : TypeN)`. +/// +/// This generates a wrapper function template. +/// All arities use this one arm - the function body is written once, not duplicated per arity. +/// Called by `_gen_method_wrapper_collect!` after it has built the full triplet list. +/// +/// The generated function returns `impl Future>> + 'a` so callers +/// can `.await` the method call, e.g. `consumer.method_name(arg0).await?`. +#[doc(hidden)] +#[macro_export] +macro_rules! _gen_method_wrapper_body { + ($me_name:ident -> $me_ret:ty ; [$(($p:ident : $g:ident : $c:ty)),+]) => { + pub fn $me_name<'a, $($g),+>( + &'a self, + $($p: $g),+ + ) -> impl core::future::Future::MethodReturnSample<$me_ret>>> + 'a + where + ($($g,)+): score_com::MethodCallInput<($($c,)+), $me_ret, R>, + R::MethodCaller<($($c,)+), $me_ret>: + score_com::MethodCaller<($($c,)+), $me_ret, R>, + { + score_com::MethodCallInput::invoke(($($p,)+), &self.$me_name) + } + }; +} + mod tests { /// ``` /// mod my_module { @@ -489,7 +885,9 @@ mod tests { #[cfg(doctest)] fn interface_macro_with_custom_id_with_comma_for_backend_compatibility() {} - /// ```compile_fail + /// Mixed interface (Event + Field + Method) with a custom ID. + /// + /// ``` /// mod my_module { /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; /// @@ -500,26 +898,28 @@ mod tests { /// const ID: &'static str = "Tire"; /// } /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// /// interface!( /// interface Vehicle { - /// Id = "CustomVehicleInterface", - /// left_tire: Method, - /// exhaust: Method, + /// Id = "AbcInterface", + /// left_tire: Event, + /// left_tire_field: Field, + /// left_tire_method(Tire) -> Tire, /// } /// ); /// } /// ``` - /// This will fail to compile because the macro does not support Method definitions and will - /// produce a compile-time error indicating that Method definitions are not supported. + /// Generates `VehicleInterface`, `VehicleConsumer`, `VehicleProducer`, + /// and `VehicleOfferedProducer` where: + /// - `VehicleConsumer` has `left_tire: Subscriber`, + /// `left_tire_field: FieldSubscriber`, + /// `left_tire_method: MethodCaller<(Tire,), Tire>`, + /// and a convenience `left_tire_method(arg0: Tire)` method. + /// - `VehicleProducer` derives `TypeStateValidator` and requires the `.init()` chain: + /// `producer.init().update_left_tire_field(&val)?.register_set_handler_left_tire_field(f).register_left_tire_method_handler(h).offer()?` + /// - `VehicleOfferedProducer` has `left_tire: Publisher` (created lazily on offer), + /// `left_tire_field: FieldPublisher`, plus the active method handler. #[cfg(doctest)] - fn interface_macro_with_Method() {} + fn interface_macro_mixed() {} /// ```compile_fail /// mod my_module { @@ -541,14 +941,36 @@ mod tests { /// /// interface!( /// interface Vehicle { - /// left_tire: Field, - /// exhaust: Field, + /// Id = "CustomVehicleInterface", + /// left_tire: Method, + /// exhaust: Method, /// } /// ); /// } /// ``` - /// This will fail to compile because the macro does not support Field definitions and will - /// produce a compile-time error indicating that Field definitions are not supported. + /// This will fail to compile because `Method` (old syntax without a return type) is not + /// supported. Use fn-like syntax: `method_name(Args) -> Ret`. + #[cfg(doctest)] + fn interface_macro_with_old_method_syntax() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface_common!(Vehicle, "CustomVehicleInterface", { + /// left_tire: Event, + /// }); + /// } + /// ``` + /// This will fail to compile because `interface_common!` does not accept member definitions. + /// Use `interface!` for a complete interface definition. #[cfg(doctest)] fn interface_macro_with_Field() {} diff --git a/score/mw/com/rust/score_com_macros/lib.rs b/score/mw/com/rust/score_com_macros/lib.rs index 09791ad12..0edf50166 100644 --- a/score/mw/com/rust/score_com_macros/lib.rs +++ b/score/mw/com/rust/score_com_macros/lib.rs @@ -337,31 +337,60 @@ fn collect_field_types(data: &Data) -> Result, ()> { Ok(out) } -/// Procedural macro to generate compile-time type-state validator for Field-based producers. +/// Unified derive macro for compile-time type-state validation of Field and Method producers. +/// It generate the validator struct and the type-state chain for the producer, +/// ensuring that all required fields and handlers are properly set before offering the service. +/// User need to call `init()` on the producer to start the type-state chain, and then call the generated +/// `update_*` and `register_set_handler_*`, `register_*_handler` methods in any order, +/// and finally call `offer()` to complete the chain. +/// user will get compile-time error if any required field or handler is not set before calling `offer()`. /// -/// This macro generates a validator struct with phantom type parameters that track -/// the initialization state of each field at compile time. The `offer()` method is only -/// available when all fields have been initialized, preventing runtime errors. +/// User no need to use this macro explicitly, +/// it will be automatically generated by the `interface!` macro for the producer struct. /// -/// # Usage +/// Note: This macro identifies member types by the last segment of each field's type path: +/// - `FieldPublisher` - generates `update_{name}()` and `register_set_handler_{name}()` +/// - `MethodHandler` - generates `register_{name}_handler()` +/// - `instance_info` field is always skipped. +/// So if member type is changed to a different type or renamed, +/// then macro need to be updated to recognize the new type name or path segment. +/// +/// # Generated validator struct +/// +/// `{Name}Validator` where: +/// - `Si` = field update state (`Uninit` / `Init`) +/// - `Hi` = field set-handler state (`HandlerNotSet` / `HandlerSet`) +/// - `Mj` = method handler state (`HandlerNotSet` / `HandlerSet`) +/// +/// `offer()` is only available when ALL `Si = Init`, ALL `Hi = HandlerSet`, ALL `Mj = HandlerSet`. /// -/// Apply this macro alongside the `interface!` macro for Field-based interfaces: +/// Entry point on the producer: `init()` - begins the type-state chain. +/// +/// Degenerates correctly: +/// - Field-only struct - no `Mj` params +/// - Method-only struct - no `Si`/`Hi` params +/// - Mixed struct - all param groups combined +/// +/// # Usage /// /// ```ignore -/// #[derive(TypeStateFieldValidator)] -/// struct VehicleFieldProducer { +/// #[derive(TypeStateValidator)] +/// struct VehicleProducer { /// left_tire: R::FieldPublisher, -/// exhaust: R::FieldPublisher, +/// process: R::MethodHandler<(Tire,), Tire>, +/// instance_info: R::ProviderInfo, /// } +/// // Generated: producer.init() +/// // .update_left_tire(&v)? +/// // .register_set_handler_left_tire(|v| {}) +/// // .register_process_handler(|req| { ... }) +/// // .offer()? /// ``` -/// -/// Macro will generate a `VehicleFieldProducerValidator` struct with phantom type parameters -/// representing the initialization state of each field and handler. The `offer()` method will only be -/// available when all fields are initialized and all handlers are registered, ensuring compile-time safety. // TODO: Document tests need to be added for this macro, including successful and failed compilation cases. -#[proc_macro_derive(TypeStateFieldValidator)] -pub fn derive_typestate_field_validator(input: TokenStream) -> TokenStream { - type_state_validator::derive_typestate_field_validator_impl(input) +// Once field or method design merged, other PR can add the tests for this macro. +#[proc_macro_derive(TypeStateValidator)] +pub fn derive_typestate_validator(input: TokenStream) -> TokenStream { + type_state_validator::derive_typestate_validator_impl(input) } // Use doctest to test failed compilations and successful ones diff --git a/score/mw/com/rust/score_com_macros/type_state_validator.rs b/score/mw/com/rust/score_com_macros/type_state_validator.rs index 038c340e8..7dac28567 100644 --- a/score/mw/com/rust/score_com_macros/type_state_validator.rs +++ b/score/mw/com/rust/score_com_macros/type_state_validator.rs @@ -13,164 +13,217 @@ use proc_macro::TokenStream; use quote::quote; -use syn::spanned::Spanned; use syn::{parse_macro_input, Data, DeriveInput, Fields, Type}; -/// The macro generates a validator struct with phantom type parameters that track -/// both the initial value update and handler registration of each field at compile time. -/// The `offer()` method is only available when all fields have been initialized and -/// all handlers have been registered, preventing runtime errors. +/// Unified type-state validator for producers containing `FieldPublisher` and/or +/// `MethodHandler` members. /// -/// It generate the field updatd method with concatenated name like `update_` -/// and register handler method with concatenated name like `register_set_handler_`. -/// e.g. for field `left_tire`, the generated methods will be `update_left_tire` and `register_set_handler_left_tire`. -pub fn derive_typestate_field_validator_impl(input: TokenStream) -> TokenStream { +/// Detects member type by the last segment of each field's type path: +/// - `FieldPublisher` - generates `update_{name}()` (Uninit - Init) and +/// `register_set_handler_{name}()` (HandlerNotSet - HandlerSet) per member. +/// - `MethodHandler` - generates `register_{name}_handler()` +/// (HandlerNotSet - HandlerSet) per member. +/// - `instance_info` field is always skipped. +/// +/// # Generated validator struct +/// +/// `{Name}Validator` where: +/// - `Si` tracks update state of field member `i` (`Uninit` / `Init`) +/// - `Hi` tracks set-handler state of field member `i` (`HandlerNotSet` / `HandlerSet`) +/// - `Mj` tracks handler state of method member `j` (`HandlerNotSet` / `HandlerSet`) +/// +/// `offer()` is only generated for the impl where ALL `Si = Init`, ALL `Hi = HandlerSet`, +/// ALL `Mj = HandlerSet`. It calls `_offer_internal()` on the wrapped producer. +/// +/// Entry point on the producer: `init()` - returns the validator with every state +/// parameter set to its initial value (`Uninit` / `HandlerNotSet`). +/// +/// Note: This macro identifies member types by the member types so if member type is changed to a different type or renamed, +/// then macro need to be updated to recognize the new type name or path segment. +pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; - // Extract runtime generic parameter + // Extract runtime generic parameter from the first generic param of the struct. let (runtime_param_name, runtime_param_with_bounds) = if let Some(param) = input.generics.params.first() { match param { syn::GenericParam::Type(type_param) => { - let name = &type_param.ident; - (quote! { #name }, quote! { #param }) + let n = &type_param.ident; + (quote! { #n }, quote! { #param }) } _ => (quote! { R }, quote! { R: score_com::Runtime + ?Sized }), } } else { (quote! { R }, quote! { R: score_com::Runtime + ?Sized }) }; - // Currently supporting only struct but in future if require will support enum. + let fields = match &input.data { Data::Struct(data) => match &data.fields { Fields::Named(fields) => &fields.named, _ => { return syn::Error::new_spanned( name, - "TypeStateFieldValidator only supports structs with named fields", + "TypeStateValidator only supports structs with named fields", ) .to_compile_error() .into(); } }, + // TODO: If require support for enum or tuple struct then add support here. _ => { - return syn::Error::new_spanned(name, "TypeStateFieldValidator only supports structs") + return syn::Error::new_spanned(name, "TypeStateValidator only supports structs") .to_compile_error() .into(); } }; - // Extract field information - use all fields except instance_info - let field_info: Vec<_> = fields - .iter() - .filter_map(|f| { - let ident = f.ident.as_ref()?; + // Classify each field by the last segment of its type path. + // Note: these string names ("FieldPublisher", "MethodHandler") must match the trait/type + // names used in the Runtime associated types. If those names change, update here too. + struct FieldMember { + ident: syn::Ident, + inner_ty: Type, // T extracted from FieldPublisher + } + struct MethodMember { + ident: syn::Ident, + args_ty: Type, // Args extracted from MethodHandler + return_ty: Type, // Return extracted from MethodHandler + } - // Skip instance_info field - // Note: type name is using here as we have same name in interface_macros - // If that change then this also need to be updated. - // Or we need to find some common solution like const name. - if ident == "instance_info" { - return None; - } + let mut field_members: Vec = Vec::new(); + let mut method_members: Vec = Vec::new(); - Some(( - ident, // struct field name - ident, // public field name (same as struct field) for methods generation. - &f.ty, // field type - )) - }) - .collect(); + for f in fields.iter() { + let ident = match f.ident.as_ref() { + Some(i) => i.clone(), + None => continue, + }; + // Skip the `instance_info` field, which is not part of the type-state validation. + if ident == "instance_info" { + continue; + } - if field_info.is_empty() { + // Note: pattern matching ("FieldPublisher", "MethodHandler") must match the trait/type + // names used in the Runtime associated types. If those names change, update here too. + if let Type::Path(type_path) = &f.ty { + if let Some(segment) = type_path.path.segments.last() { + match segment.ident.to_string().as_str() { + "FieldPublisher" => { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { + field_members.push(FieldMember { + ident, + inner_ty: inner.clone(), + }); + } + } + } + "MethodHandler" => { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if args.args.len() >= 2 { + if let ( + Some(syn::GenericArgument::Type(args_ty)), + Some(syn::GenericArgument::Type(return_ty)), + ) = (args.args.get(0), args.args.get(1)) + { + method_members.push(MethodMember { + ident, + args_ty: args_ty.clone(), + return_ty: return_ty.clone(), + }); + } + } + } + } + _ => {} // Other fields (e.g. PhantomData) are ignored. + } + } + } + } + // If no FieldPublisher or MethodHandler members were found, emit a compile error. + // because macro is only added to producer struct which has at least one FieldPublisher or MethodHandler member. + if field_members.is_empty() && method_members.is_empty() { return syn::Error::new_spanned( name, - "No fields found for validation (excluding instance_info)", + "TypeStateValidator: no FieldPublisher or MethodHandler fields found \ + (excluding instance_info)", ) .to_compile_error() .into(); } - let struct_field_names: Vec<_> = field_info.iter().map(|(sf, _, _)| sf).collect(); - let public_field_names: Vec<_> = field_info.iter().map(|(_, pf, _)| pf).collect(); - let field_types: Vec<_> = field_info.iter().map(|(_, _, ty)| ty).collect(); + let validator_name = syn::Ident::new(&format!("{}Validator", name), name.span()); - // Extract inner types from R::FieldPublisher -> T - let inner_types: Vec<_> = field_types - .iter() - .map(|ty| { - // Try to extract T from R::FieldPublisher - if let Type::Path(type_path) = ty { - // Look for the last segment which should be FieldPublisher - if let Some(segment) = type_path.path.segments.last() { - //Note: Same here we are using trait name directly - // But if that change then this also need to be updated. - if segment.ident == "FieldPublisher" { - // Extract the type argument - if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { - return inner_ty; - } - } - } - } - } - // Fallback: use the full type - *ty - }) + // State param naming: + // S{i} — update state for field member i (Uninit / Init) + // H{i} — set-handler state for field member i (HandlerNotSet / HandlerSet) + // M{j} — handler state for method member j (HandlerNotSet / HandlerSet) + // Combined order in the validator struct: [S0..Sn, H0..Hn, M0..Mm] + let field_update_params: Vec = (0..field_members.len()) + .map(|i| syn::Ident::new(&format!("S{}", i), proc_macro::Span::call_site().into())) + .collect(); + let field_handler_params: Vec = (0..field_members.len()) + .map(|i| syn::Ident::new(&format!("H{}", i), proc_macro::Span::call_site().into())) + .collect(); + let method_handler_params: Vec = (0..method_members.len()) + .map(|j| syn::Ident::new(&format!("M{}", j), proc_macro::Span::call_site().into())) .collect(); - // Generate the validator struct name - e.g., for VehicleProducer, the validator will be VehicleValidator - let validator_name = syn::Ident::new(&format!("{}Validator", name), name.span()); - // Generate type parameters for each field's UPDATE state (S0, S1, S2, ...) - let field_update_state_params: Vec<_> = public_field_names + // Flat list used in struct definition and impl generics: [S0..Sn, H0..Hn, M0..Mm] + let all_params: Vec<&syn::Ident> = field_update_params .iter() - .enumerate() - .map(|(i, _)| syn::Ident::new(&format!("S{}", i), proc_macro::Span::call_site().into())) + .chain(field_handler_params.iter()) + .chain(method_handler_params.iter()) .collect(); - // Generate type parameters for each field's HANDLER state (H0, H1, H2, ...) - let field_handler_state_params: Vec<_> = public_field_names - .iter() - .enumerate() - .map(|(i, _)| syn::Ident::new(&format!("H{}", i), proc_macro::Span::call_site().into())) + // Initial states for init() entry point. + let init_states: Vec<_> = (0..field_members.len()) + .map(|_| quote! { ::score_com::Uninit }) + .chain((0..field_members.len()).map(|_| quote! { ::score_com::HandlerNotSet })) + .chain((0..method_members.len()).map(|_| quote! { ::score_com::HandlerNotSet })) .collect(); - // Generate update methods - each one changes its field's UPDATE state from current to Init - // while preserving HANDLER state - let update_methods = public_field_names + // All-satisfied states required by offer(). + let done_states: Vec<_> = (0..field_members.len()) + .map(|_| quote! { ::score_com::Init }) + .chain((0..field_members.len()).map(|_| quote! { ::score_com::HandlerSet })) + .chain((0..method_members.len()).map(|_| quote! { ::score_com::HandlerSet })) + .collect(); + + // update_{name}() impls for each field member + // Transitions Si: Uninit - Init while all other state params stay generic. + let update_methods: Vec<_> = field_members .iter() - .zip(struct_field_names.iter()) - .zip(inner_types.iter()) .enumerate() - .map(|(i, ((pub_name, struct_name), inner_ty))| { - // Generate the method name for updating this field - e.g., update_left_tire for field left_tire - let update_fn = syn::Ident::new(&format!("update_{}", pub_name), pub_name.span()); + .map(|(i, member)| { + let update_fn = + syn::Ident::new(&format!("update_{}", member.ident), member.ident.span()); + let inner_ty = &member.inner_ty; + let field_ident = &member.ident; - // Build the "after" UPDATE state parameter list where this field is Init - let after_update_states: Vec<_> = field_update_state_params + // After-state list: Si becomes Init, every other param stays generic. + let after: Vec<_> = all_params .iter() .enumerate() - .map(|(j, param)| { - if i == j { + .map(|(k, p)| { + if k == i { quote! { ::score_com::Init } } else { - quote! { #param } + quote! { #p } } }) .collect(); quote! { - impl<#runtime_param_with_bounds, #(#field_update_state_params),*, #(#field_handler_state_params),*> - #validator_name<#runtime_param_name, #(#field_update_state_params),*, #(#field_handler_state_params),*> + impl<#runtime_param_with_bounds, #(#all_params),*> + #validator_name<#runtime_param_name, #(#all_params),*> { pub fn #update_fn( mut self, - value: &#inner_ty - ) -> score_com::Result<#validator_name<#runtime_param_name, #(#after_update_states),*, #(#field_handler_state_params),*>> - { - self.producer.#struct_name.update(value)?; + value: &#inner_ty, + ) -> score_com::Result<#validator_name<#runtime_param_name, #(#after),*>> { + self.producer.#field_ident.update(value)?; Ok(#validator_name { producer: self.producer, _phantom: core::marker::PhantomData, @@ -178,94 +231,146 @@ pub fn derive_typestate_field_validator_impl(input: TokenStream) -> TokenStream } } } - }); + }) + .collect(); - // Generate register_set_handler methods - each one changes its field's HANDLER state - // from HandlerNotSet to HandlerSet while preserving UPDATE state - let register_handler_methods = public_field_names + // register_set_handler_{name}() impls for each field member + // Hi is at index field_members.len() + i in all_params. + // Transitions Hi: HandlerNotSet - HandlerSet while all other state params stay generic. + let register_set_handler_methods: Vec<_> = field_members .iter() - .zip(struct_field_names.iter()) - .zip(inner_types.iter()) .enumerate() - .map(|(i, ((pub_name, struct_name), inner_ty))| { + .map(|(i, member)| { let register_fn = syn::Ident::new( - &format!("register_set_handler_{}", pub_name), - pub_name.span(), + &format!("register_set_handler_{}", member.ident), + member.ident.span(), ); + let inner_ty = &member.inner_ty; + let field_ident = &member.ident; + let hi_index = field_members.len() + i; - // Build the "after" HANDLER state parameter list where this field is HandlerSet - let after_handler_states: Vec<_> = field_handler_state_params + let after: Vec<_> = all_params .iter() .enumerate() - .map(|(j, param)| { - if i == j { + .map(|(k, p)| { + if k == hi_index { quote! { ::score_com::HandlerSet } } else { - quote! { #param } + quote! { #p } } }) .collect(); quote! { - impl<#runtime_param_with_bounds, #(#field_update_state_params),*, #(#field_handler_state_params),*> - #validator_name<#runtime_param_name, #(#field_update_state_params),*, #(#field_handler_state_params),*> + impl<#runtime_param_with_bounds, #(#all_params),*> + #validator_name<#runtime_param_name, #(#all_params),*> where <#runtime_param_name as score_com::Runtime>::FieldPublisher<#inner_ty>: Send, { - pub fn #register_fn(mut self, handler: F) -> score_com::Result<#validator_name<#runtime_param_name, #(#field_update_state_params),*, #(#after_handler_states),*>> + pub fn #register_fn( + mut self, + handler: F, + ) -> #validator_name<#runtime_param_name, #(#after),*> where F: Fn(&#inner_ty) + Send + 'static, { - self.producer.#struct_name.register_set_handler(handler)?; - Ok(#validator_name { + self.producer.#field_ident.register_set_handler(handler); + #validator_name { producer: self.producer, _phantom: core::marker::PhantomData, - }) + } } } } - }); - - // Generate list of all Init states for the offer() impl - let all_init_states = vec![quote! { ::score_com::Init }; field_update_state_params.len()]; + }) + .collect(); - // Generate list of all HandlerSet states for the offer() impl - let all_handler_set_states = - vec![quote! { ::score_com::HandlerSet }; field_handler_state_params.len()]; + // register_{name}_handler() impls for each method member + // Mj is at index 2 * field_members.len() + j in all_params. + // Transitions Mj: HandlerNotSet - HandlerSet while all other state params stay generic. + let register_handler_methods: Vec<_> = method_members + .iter() + .enumerate() + .map(|(j, member)| { + let register_fn = syn::Ident::new( + &format!("register_{}_handler", member.ident), + member.ident.span(), + ); + let args_ty = &member.args_ty; + let return_ty = &member.return_ty; + let method_ident = &member.ident; + let mj_index = 2 * field_members.len() + j; - // Generate list of all Uninit states for the validator() method - let all_uninit_states = vec![quote! { ::score_com::Uninit }; field_update_state_params.len()]; + let after: Vec<_> = all_params + .iter() + .enumerate() + .map(|(k, p)| { + if k == mj_index { + quote! { ::score_com::HandlerSet } + } else { + quote! { #p } + } + }) + .collect(); - // Generate list of all HandlerNotSet states for the validator() method - let all_handler_not_set_states = - vec![quote! { ::score_com::HandlerNotSet }; field_handler_state_params.len()]; + quote! { + impl<#runtime_param_with_bounds, #(#all_params),*> + #validator_name<#runtime_param_name, #(#all_params),*> + { + pub fn #register_fn( + mut self, + handler: F, + ) -> #validator_name<#runtime_param_name, #(#after),*> + where + F: score_com::MethodHandlerCall<#args_ty, #return_ty>, + { + <_ as score_com::MethodHandler<#args_ty, #return_ty, #runtime_param_name>>::register_handler( + &self.producer.#method_ident, + handler, + ); + #validator_name { + producer: self.producer, + _phantom: core::marker::PhantomData, + } + } + } + } + }) + .collect(); let expanded = quote! { - // Validator struct with dual type-state tracking: - // - First set of params (S0, S1, ...) track field UPDATE state (Uninit/Init) - // - Second set of params (H0, H1, ...) track HANDLER registration state (HandlerNotSet/HandlerSet) - pub struct #validator_name<#runtime_param_with_bounds, #(#field_update_state_params),*, #(#field_handler_state_params),*> { + // Validator struct type params track state of every Field and Method member. + // Layout: + pub struct #validator_name<#runtime_param_with_bounds, #(#all_params),*> { producer: #name<#runtime_param_name>, - _phantom: core::marker::PhantomData<(#(#field_update_state_params,)* #(#field_handler_state_params,)*)>, + _phantom: core::marker::PhantomData<(#(#all_params,)*)>, } - // Update methods that change UPDATE state types (Uninit -> Init) + // update_{name}() - transitions Si: Uninit - Init #(#update_methods)* - // Register set handler methods that change HANDLER state types (HandlerNotSet -> HandlerSet) + // register_set_handler_{name}() - transitions Hi: HandlerNotSet - HandlerSet + #(#register_set_handler_methods)* + + // register_{name}_handler() - transitions Mj: HandlerNotSet - HandlerSet #(#register_handler_methods)* - // offer() is only available when ALL fields are Init AND all handlers are HandlerSet - impl<#runtime_param_with_bounds> #validator_name<#runtime_param_name, #(#all_init_states),*, #(#all_handler_set_states),*> { - pub fn offer(self) -> score_com::Result<<#name<#runtime_param_name> as score_com::Producer<#runtime_param_name>>::OfferedProducer> { - // Call internal offer implementation after validating all fields are initialized and handlers registered + // offer() is only available when ALL Si = Init, ALL Hi = HandlerSet, ALL Mj = HandlerSet. + impl<#runtime_param_with_bounds> + #validator_name<#runtime_param_name, #(#done_states),*> + { + pub fn offer( + self, + ) -> score_com::Result<<#name<#runtime_param_name> as score_com::Producer<#runtime_param_name>>::OfferedProducer> { self.producer._offer_internal() } } - // init_field() method consumes producer and returns validator with all fields Uninit and all handlers HandlerNotSet + // init() - entry point on the original producer, begins the type-state chain. impl<#runtime_param_with_bounds> #name<#runtime_param_name> { - pub fn init_field(self) -> #validator_name<#runtime_param_name, #(#all_uninit_states),*, #(#all_handler_not_set_states),*> { + pub fn init( + self, + ) -> #validator_name<#runtime_param_name, #(#init_states),*> { #validator_name { producer: self, _phantom: core::marker::PhantomData, From 52b559d73e1c1de131680928324ff90a9962f913 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Wed, 29 Jul 2026 16:12:09 +0530 Subject: [PATCH 16/25] Rust::com Update Field and Method documentation --- .../com-api-gen/com_api_gen.rs | 24 ++++++------ .../com-api-example/src/field_producer.rs | 2 + .../com-api-runtime-lola/field_consumer.rs | 1 + .../com-api-runtime-lola/field_producer.rs | 4 +- .../com-api/com-api-runtime-mock/runtime.rs | 38 ++++++++++++++++++- .../com/rust/design/design_document_method.md | 4 +- .../com/rust/design/method_trait_diagram.puml | 1 + score/mw/com/rust/score_com_concept/BUILD | 2 +- .../mw/com/rust/score_com_concept/concept.rs | 5 ++- .../rust/score_com_concept/field_concept.rs | 9 ++--- .../method_arities_macros.rs | 7 ++-- .../rust/score_com_concept/method_concept.rs | 4 +- .../score_com_macros/type_state_validator.rs | 10 ++--- 13 files changed, 74 insertions(+), 37 deletions(-) diff --git a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs index f9ec6c10f..2a627d5c8 100644 --- a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs +++ b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs @@ -85,15 +85,15 @@ interface!( ); // We can also define mix of event , field and method in one interface. -// interface!( -// interface VehicleMonitor { -// Id = "VehicleMonitorInterface", -// left_tire: Event, -// exhaust: Event, -// left_tire_field: Field, -// exhaust_field: Field, -// update_tire_pressure(Tire) -> (), -// update_front_tires_pressure(Tire, Tire) -> (), -// get_tire_pressure() -> Tire, -// } -// ); +interface!( + interface VehicleMonitor { + Id = "VehicleMonitorInterface", + left_tire: Event, + exhaust: Event, + left_tire_field: Field, + exhaust_field: Field, + update_tire_pressure(Tire) -> (), + update_front_tires_pressure(Tire, Tire) -> (), + get_tire_pressure() -> Tire, + } +); diff --git a/score/mw/com/example/com-api-example/src/field_producer.rs b/score/mw/com/example/com-api-example/src/field_producer.rs index 7b60d50fb..fa360bd80 100644 --- a/score/mw/com/example/com-api-example/src/field_producer.rs +++ b/score/mw/com/example/com-api-example/src/field_producer.rs @@ -54,9 +54,11 @@ where // TODO: in working example add that logic to demonstrate the set handler usage. // Note: I think producer may be need clone ? }) + .expect("Failed to register set handler for left_tire") .register_set_handler_exhaust(|_val: &Exhaust| { println!("Received exhaust update"); }) + .expect("Failed to register set handler for exhaust") .update_left_tire(&initial_tire_value) .expect("Failed to update left_tire field") .update_exhaust(&initial_exhaust_value) diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs index 81e0a5d77..8a57d9f3d 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs @@ -18,6 +18,7 @@ //! we will create a module which will have common trait for event and field which will be used by both event and field consumer/publisher. use core::fmt::Debug; +use core::future::Future; use core::marker::PhantomData; use bridge_ffi_rs::FFIBridge; diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs index 14518dd82..06a867912 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs @@ -86,7 +86,7 @@ impl FieldPublisher> where Self: 'a; - fn new(_identifier: &str, _instance_info: LolaProviderInfo) -> Result { + fn new(_identifier: &'static str, _instance_info: LolaProviderInfo) -> Result { todo!() } fn allocate(&self) -> Result> { @@ -95,7 +95,7 @@ impl FieldPublisher> fn update(&self, _value: &T) -> Result<()> { todo!() } - fn register_set_handler<'a>(&self, _callback: impl Fn(&T) + Send + 'a) -> Result<()> { + fn register_set_handler(&self, _callback: impl Fn(&T) + Send + 'static) -> Result<()> { //If waker get the notification form FFI call then //Create a task to call the callback with value. //Thread pool is a option here to run the callback in a separate thread. diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs index 587a9faff..8afd55b0f 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs @@ -519,6 +519,40 @@ impl RuntimeBuilderImpl { } } +// Mock zero-copy allocator types for method arguments. +pub struct MockMethodInArgMaybeUninit { + _phantom: PhantomData, +} + +pub struct MockMethodInArgPtr { + _phantom: PhantomData, +} + +impl MethodInArgPtr for MockMethodInArgPtr {} + +impl MethodInArgMaybeUninit for MockMethodInArgMaybeUninit { + type Ptr = MockMethodInArgPtr; + + fn write(self, _val: T) -> ZeroCopyArgs> { + todo!() + } + + unsafe fn assume_init(self) -> ZeroCopyArgs> { + todo!() + } +} + +pub struct MockMethodInArgAllocator; + +impl MethodInArgAllocator for MockMethodInArgAllocator { + type MethodInArgPtr = MockMethodInArgPtr; + type MethodInArgMaybeUninit = MockMethodInArgMaybeUninit; + + fn allocate(&self) -> MockMethodInArgMaybeUninit { + todo!() + } +} + pub struct MockMethodHandler { _phantom: core::marker::PhantomData<(Args, Return, R)>, } @@ -774,7 +808,7 @@ impl FieldPublisher for MockFieldPublis where Self: 'a; - fn new(_identifier: &str, _instance_info: MockProviderInfo) -> Result { + fn new(_identifier: &'static str, _instance_info: MockProviderInfo) -> Result { Ok(Self { _data: PhantomData }) } @@ -789,7 +823,7 @@ impl FieldPublisher for MockFieldPublis todo!() } - fn register_set_handler<'a>(&self, _callback: impl Fn(&T) + Send + 'a) -> Result<()> { + fn register_set_handler(&self, _callback: impl Fn(&T) + Send + 'static) -> Result<()> { todo!() } } diff --git a/score/mw/com/rust/design/design_document_method.md b/score/mw/com/rust/design/design_document_method.md index ae69b2334..a3beab12e 100644 --- a/score/mw/com/rust/design/design_document_method.md +++ b/score/mw/com/rust/design/design_document_method.md @@ -34,7 +34,7 @@ This document describes the design of the **method** APIs and usage of it. ## Overview -Rust Communication library provide the Method based communication pattern (mostly with alignment of c++ APIs), followings are major points +Rust Communication library provides the Method based communication pattern (mostly with alignment of c++ APIs), followings are major points of the design- - Method calls are always async and every generated wrapper returns `impl Future` and must be `.await`ed. - Arguments can be passed by value (copy path) or via pre-allocated pointers (zero-copy path) using the same call site and the compiler selects the correct dispatch based on argument type. @@ -350,7 +350,7 @@ interface!( For each method, the macro generates: **On `VehicleMethodsConsumer`**-a callable wrapper field. The field is a struct that: -- Implements `AsyncFn`semantics so `consumer.update_tire_pressure(arg).await` works +- Implements `AsyncFn` semantics so `consumer.update_tire_pressure(arg).await` works - Exposes `.allocate()` for the zero-copy path - Holds a reference to the runtime's `R::MethodCaller<(Tire,), ()>` instance - Calls through `MethodCallInput::invoke()` to dispatch to the correct `MethodCaller` method diff --git a/score/mw/com/rust/design/method_trait_diagram.puml b/score/mw/com/rust/design/method_trait_diagram.puml index 511f5b15e..246fef05c 100644 --- a/score/mw/com/rust/design/method_trait_diagram.puml +++ b/score/mw/com/rust/design/method_trait_diagram.puml @@ -113,6 +113,7 @@ interface "MethodInArgPtr" as MethodInArgPtr { ' Runtime implements on its concrete type: ' LolaMethodInArgPtr, MockMethodInArgPtr ' Concrete type will hold FFI slot pointer + Drop +} class "ZeroCopyArgs

" as ZeroCopyArgs { + 0: P diff --git a/score/mw/com/rust/score_com_concept/BUILD b/score/mw/com/rust/score_com_concept/BUILD index 70443f8cf..c7447ef4d 100644 --- a/score/mw/com/rust/score_com_concept/BUILD +++ b/score/mw/com/rust/score_com_concept/BUILD @@ -57,7 +57,7 @@ rust_unit_test( name = "score_com_concept-macros-unit-tests", srcs = ["interface_macros.rs"], features = ["link_std_cpp_lib"], - # TODO: uncomment this once field or method one PR is merged, + # TODO: remove tags = ["manual"] once field or method PR is merged, # Unit test failed because macro has field and method both types tags = ["manual"], deps = ["//score/mw/com/rust:score_com"], diff --git a/score/mw/com/rust/score_com_concept/concept.rs b/score/mw/com/rust/score_com_concept/concept.rs index 5a41afd70..6ce9a4ad4 100644 --- a/score/mw/com/rust/score_com_concept/concept.rs +++ b/score/mw/com/rust/score_com_concept/concept.rs @@ -104,6 +104,7 @@ pub trait Runtime { /// `Publisher` types for Publishes event data to subscribers type Publisher: Publisher; + /// `MethodInArgAllocator` allocates pre-initialized argument slots for the zero-copy method call path type MethodInArgAllocator: MethodInArgAllocator; /// `MethodReturnSample` wraps the return value of a method call. @@ -115,7 +116,7 @@ pub trait Runtime { /// `MethodHandler` types for handling method calls on the skeleton/producer side type MethodHandler: MethodHandler; - /// `FieldSubscription` types for Manages subscriptions to field instance + /// `FieldSubscriber` types for Manages subscriptions to field instance type FieldSubscriber: FieldSubscriber; /// `FieldPublisher` types for Publishes field constructs and update the data @@ -231,7 +232,7 @@ pub trait CommData: Reloc { const ID: &'static str; } -// Arity-0 unit tuple hereand other arities 1+ are generated by `impl_all_arities!` in `method_arities.rs`. +// Arity-0 unit tuple here and other arities 1+ are generated by `impl_all_arities!` in `method_arities_macros.rs`. impl CommData for () { const ID: &'static str = "()"; } diff --git a/score/mw/com/rust/score_com_concept/field_concept.rs b/score/mw/com/rust/score_com_concept/field_concept.rs index 1bedb35cc..65b98309c 100644 --- a/score/mw/com/rust/score_com_concept/field_concept.rs +++ b/score/mw/com/rust/score_com_concept/field_concept.rs @@ -65,7 +65,7 @@ pub trait FieldSubscriber: fn set(&self, value: &T) -> Result>; } -/// FieldSubscriber trait is provides the receiving APIs for the field subscription and +/// FieldSubscription trait provides the receiving APIs for the field subscription and /// it is derived from `concept::Subscription` trait which provides the receiving APIs for the field subscription. /// Additional methods which the field subscription provides are added in this trait. pub trait FieldSubscription: @@ -80,7 +80,7 @@ pub trait FieldSubscription: /// This is for checking the capacity of the field subscription and to avoid overflow of the field subscription limit. fn get_free_sample_count(&self) -> Result; - ///Get the current value of the field. + /// Get the current value of the field. /// /// #returns /// Return the `Future>>` which contains the current value of the field. @@ -105,7 +105,7 @@ pub trait FieldPublisher { Self: 'a; /// Create a new publisher for the specified event source. - fn new(identifier: &str, instance_info: R::ProviderInfo) -> Result + fn new(identifier: &'static str, instance_info: R::ProviderInfo) -> Result where Self: Sized; @@ -131,8 +131,7 @@ pub trait FieldPublisher { /// /// # Returns /// Return the result of `Result<()>` which contains the status of the register operation. - // TODO: Do we need to make callback lifetime 'static or we keep same as field publisher lifetime. - fn register_set_handler<'a>(&self, callback: impl Fn(&T) + Send + 'a) -> Result<()>; + fn register_set_handler(&self, callback: impl Fn(&T) + Send + 'static) -> Result<()>; } /// FieldSampleMut trait is used to update the value of the field sample for zero-copy API. diff --git a/score/mw/com/rust/score_com_concept/method_arities_macros.rs b/score/mw/com/rust/score_com_concept/method_arities_macros.rs index f92b65f47..cdb61a906 100644 --- a/score/mw/com/rust/score_com_concept/method_arities_macros.rs +++ b/score/mw/com/rust/score_com_concept/method_arities_macros.rs @@ -18,7 +18,7 @@ //! To raise (or lower) the maximum number of arguments a method may have, edit the //! `impl_all_arities!` invocation at the bottom of this file. Add one //! more `(TypeIdent, arg_ident)` pair per additional argument. Everything else - -//! `Reloc`, `CommData`, `MethodArgs`, `MethodArgsAllocate`, `MethodCallInput` (zero-copy +//! `Reloc`, `CommData`, `MethodArgs`, `MethodArgsPtrTuple`, `MethodArgsAllocate`, `MethodCallInput` (zero-copy //! path), and `MethodHandlerCall` - is generated using macros. //! //! Note: `_gen_method_wrapper!` in `interface_macros.rs` self-generates its argument @@ -29,7 +29,7 @@ //! //! # Arity 0 special case //! -//! Arity 0 (`()`) is handled separately in `com_api_method.rs` because the zero-tuple +//! Arity 0 (`()`) is handled separately in `method_concept.rs` because the zero-tuple //! has no positional variables to destructure. //! This macro covers arities 1 through 8 (inclusive) by default, but can be extended to higher arities if needed. @@ -137,8 +137,7 @@ macro_rules! impl_all_arities { // Single configuration point // // To raise the maximum method argument count: -// 1. Add one more `(TypeIdent, arg_ident)` pair below. -// 2. That's it - all six trait impls are generated automatically. +// Add one more `(TypeIdent, arg_ident)` pair below. // // `_gen_method_wrapper!` (`interface_macros.rs`) has no fixed arity limit of its own, // so raising the limit here is the only change needed. diff --git a/score/mw/com/rust/score_com_concept/method_concept.rs b/score/mw/com/rust/score_com_concept/method_concept.rs index 12dfecd40..562a093aa 100644 --- a/score/mw/com/rust/score_com_concept/method_concept.rs +++ b/score/mw/com/rust/score_com_concept/method_concept.rs @@ -284,7 +284,7 @@ pub trait MethodArgsAllocate: MethodArgs { } // Arity-0 unit tuple - special-cased here; arities 1+ are generated by -// `impl_all_arities!` in `method_arities.rs`. +// `impl_all_arities!` in `method_arities_macros.rs`. impl MethodArgsAllocate for () { type UninitTuple = (); fn alloc_uninit(_allocator: &A) {} @@ -354,7 +354,7 @@ pub trait MethodHandlerCall: Send + Sync + 'static { } // Arity-0 unit tuple - special-cased here; arities 1+ are generated by -// `impl_all_arities!` in `method_arities.rs`. +// `impl_all_arities!` in `method_arities_macros.rs`. impl MethodHandlerCall<(), Return> for F where F: Fn() -> Return + Send + Sync + 'static, diff --git a/score/mw/com/rust/score_com_macros/type_state_validator.rs b/score/mw/com/rust/score_com_macros/type_state_validator.rs index 7dac28567..d70bf22f0 100644 --- a/score/mw/com/rust/score_com_macros/type_state_validator.rs +++ b/score/mw/com/rust/score_com_macros/type_state_validator.rs @@ -270,15 +270,15 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { pub fn #register_fn( mut self, handler: F, - ) -> #validator_name<#runtime_param_name, #(#after),*> + ) -> score_com::Result<#validator_name<#runtime_param_name, #(#after),*>> where F: Fn(&#inner_ty) + Send + 'static, { - self.producer.#field_ident.register_set_handler(handler); - #validator_name { + self.producer.#field_ident.register_set_handler(handler)?; + Ok(#validator_name { producer: self.producer, _phantom: core::marker::PhantomData, - } + }) } } } @@ -318,7 +318,7 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { #validator_name<#runtime_param_name, #(#all_params),*> { pub fn #register_fn( - mut self, + self, handler: F, ) -> #validator_name<#runtime_param_name, #(#after),*> where From 0bde0fc219d7713aa5f46adcaf98f6aa23a91883 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Thu, 30 Jul 2026 10:57:33 +0530 Subject: [PATCH 17/25] Rust::com Update Field Methods using Method interface * Removed the separate set-get method and introduced field method using Method interface --- .../com-api-example/src/field_consumer.rs | 91 +++++++------------ .../com-api-runtime-lola/field_consumer.rs | 19 +--- .../com-api/com-api-runtime-mock/runtime.rs | 20 +--- .../rust/score_com_concept/field_concept.rs | 84 ++++++----------- .../score_com_concept/interface_macros.rs | 47 ++++++++++ 5 files changed, 113 insertions(+), 148 deletions(-) diff --git a/score/mw/com/example/com-api-example/src/field_consumer.rs b/score/mw/com/example/com-api-example/src/field_consumer.rs index bc332f6f6..9cae42b6f 100644 --- a/score/mw/com/example/com-api-example/src/field_consumer.rs +++ b/score/mw/com/example/com-api-example/src/field_consumer.rs @@ -14,7 +14,7 @@ #![allow(unused)] use score_com::{ - Builder, FieldSubscriber, FieldSubscription, FindServiceSpecifier, InstanceSpecifier, + Builder, FindServiceSpecifier, InstanceSpecifier, Interface, Runtime, SampleContainer, ServiceDiscovery, Subscriber, Subscription, }; @@ -45,75 +45,48 @@ fn create_consumer_field( .expect("Failed to build consumer instance") } -async fn process_get_method_async(subscription: S) -where - S: FieldSubscription, - R: Runtime, -{ - // Get field value asynchronously - match subscription.get().await { - Ok(_method_return) => { - println!("Current tire pressure from spawned task"); - } +// Function to demonstrate the usage of the consumer to get and set fields, +// and subscribe to field update notifications. +// +// Field get/set are now fully async via the MethodCaller-based wrappers generated by the +// interface! macro. They live on the consumer struct as separate fields (`{name}_get`, +// `{name}_set`) and are independent of subscribe() - they remain usable before and after. +// +// Because subscribe() takes `left_tire` by value, extract the callers before subscribing +// if both are needed in the same async context. +async fn consumer_processing_field(consumer: VehicleFieldConsumer) { + // Async get via the generated get_left_tire() wrapper. + // Uses MethodCaller<(), Tire> under the hood - reuses Method infrastructure. + match consumer.get_left_tire().await { + Ok(result) => println!("Current tire pressure (async get): {:?}", *result), Err(e) => eprintln!("Failed to get tire pressure: {:?}", e), } - println!("Async subscription processing in spawned task completed"); -} - -// Function to demonstrate the usage of the consumer to get and set fields, -// Subscribe to the fields event and it provides the set and get method as well. -fn consumer_processing_field(consumer: VehicleFieldConsumer) -where - <::FieldSubscriber as Subscriber>::Subscription: Send + 'static, -{ - // Field consumer API methods - // But they demonstrate the correct API usage pattern - // TODO: Currently we are not offering the get method async in FieldSubscriber - // because async call will may run in different thread and that will cause the issue in subscription. - let _ = consumer - .left_tire - .get() - .map(|result| println!("Got field value via consumer: {:?}", result)); - - let _ = consumer - .left_tire - .set(&Tire { pressure: 30.0 }) - .map(|result| println!("Set field value via consumer: {:?}", result)); + // Async set via the generated set_left_tire(val) wrapper. + // Uses MethodCaller<(Tire,), Tire> under the hood - reuses Method infrastructure. + match consumer.set_left_tire(Tire { pressure: 35.0 }).await { + Ok(result) => println!("Confirmed tire pressure after set: {:?}", *result), + Err(e) => eprintln!("Failed to set tire pressure: {:?}", e), + } - // Subscribe to the field to receive updates + // Subscribe to the field to receive update notifications (field value changes). + // Note: subscribe() takes `left_tire` by value. The get/set callers are separate + // struct fields so they are not consumed here, but the consumer is partially moved. let subscription = consumer .left_tire .subscribe(3) .expect("Failed to subscribe to field"); - // Create scope for sample_buf to ensure it's dropped before tokio::spawn - { - let mut sample_buf = SampleContainer::new(3); - - // Poll for updates (non-blocking) - match subscription.try_receive(&mut sample_buf, 1) { - Ok(n) if n > 0 => { - while let Some(sample) = sample_buf.pop_front() { - println!("Updated tire pressure: {:?}", *sample); - } - } - _ => { - println!("No new tire pressure updates available"); + // Poll for updates (non-blocking) + let mut sample_buf = SampleContainer::new(3); + match subscription.try_receive(&mut sample_buf, 1) { + Ok(n) if n > 0 => { + while let Some(sample) = sample_buf.pop_front() { + println!("Updated tire pressure: {:?}", *sample); } } - // sample_buf is dropped here at end of scope + _ => println!("No new tire pressure updates available"), } - // Set via subscription - let _ = subscription - .set(&Tire { pressure: 35.0 }) - .map(|result| println!("Set field value via subscription: {:?}", result)); - - // Spawn async task with subscription - // The subscription is moved into the task - tokio::spawn(async move { - process_get_method_async(subscription).await; - // subscription is automatically unsubscribed when dropped at end of task - }); + // subscription is automatically unsubscribed when dropped } diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs index 8a57d9f3d..5f72e1311 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs @@ -18,13 +18,11 @@ //! we will create a module which will have common trait for event and field which will be used by both event and field consumer/publisher. use core::fmt::Debug; -use core::future::Future; use core::marker::PhantomData; use bridge_ffi_rs::FFIBridge; use score_com_concept::{ - CommData, FieldSubscriber, FieldSubscription, MethodReturnTypePtr, Result, SampleContainer, - Subscriber, Subscription, + CommData, FieldSubscriber, FieldSubscription, Result, SampleContainer, Subscriber, Subscription, }; use crate::consumer::LolaSample; @@ -38,15 +36,11 @@ pub struct LolaFieldSubscriber { } /// Marker implementation of FieldSubscriber trait. +/// Field get/set are exposed as async MethodCaller-based wrappers on the consumer struct +/// generated by the interface! macro; they are not part of FieldSubscriber itself. impl FieldSubscriber> for LolaFieldSubscriber { - fn get(&self) -> Result> { - todo!() - } - fn set(&self, _value: &T) -> Result> { - todo!() - } } /// Implementation of Subscriber trait which provides `new` and `subscribe` methods for LolaFieldSubscriber. @@ -80,13 +74,6 @@ impl FieldSubscription> fn get_num_new_samples_available(&self) -> Result { todo!() } - - fn get(&self) -> impl Future>> + Send { - async { todo!() } - } - fn set(&self, _value: &T) -> Result> { - todo!() - } } /// Implementation of Subscription trait which provides receiving APIs. diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs index 8afd55b0f..d6a95b8c8 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs @@ -40,7 +40,7 @@ use score_com_concept::{ FieldPublisher, FieldSampleMut, FieldSubscriber, FieldSubscription, FindServiceSpecifier, InstanceSpecifier, Interface, MethodArgs, MethodArgsAllocate, MethodArgsPtrTuple, MethodCaller, MethodHandler, MethodHandlerCall, MethodInArgAllocator, MethodInArgMaybeUninit, MethodInArgPtr, - MethodReturnSample, MethodReturnTypePtr, Producer, ProducerBuilder, ProviderInfo, Publisher, + MethodReturnSample, Producer, ProducerBuilder, ProviderInfo, Publisher, Result, Runtime, RuntimeBuilder, Sample, SampleContainer, SampleMaybeUninit as SampleMaybeUninitTrait, SampleMaybeUninit, SampleMut, ServiceDiscovery, Subscriber, Subscription, ZeroCopyArgs, @@ -645,14 +645,9 @@ pub struct MockFieldSubscriber { } /// Marker implementation of FieldSubscriber trait. -impl FieldSubscriber for MockFieldSubscriber { - fn get(&self) -> Result> { - todo!() - } - fn set(&self, _value: &T) -> Result> { - todo!() - } -} +/// Field get/set are exposed as async MethodCaller-based wrappers on the consumer struct +/// generated by the interface! macro; they are not part of FieldSubscriber itself. +impl FieldSubscriber for MockFieldSubscriber {} /// Implementation of Subscriber trait for MockFieldSubscriber. impl Subscriber for MockFieldSubscriber { @@ -690,13 +685,6 @@ impl FieldSubscription for MockFieldSub fn get_num_new_samples_available(&self) -> Result { todo!() } - - fn get(&self) -> impl Future>> + Send { - async { todo!() } - } - fn set(&self, _value: &T) -> Result> { - todo!() - } } /// Implementation of Subscription trait which provides receiving APIs. diff --git a/score/mw/com/rust/score_com_concept/field_concept.rs b/score/mw/com/rust/score_com_concept/field_concept.rs index 65b98309c..f714a94fd 100644 --- a/score/mw/com/rust/score_com_concept/field_concept.rs +++ b/score/mw/com/rust/score_com_concept/field_concept.rs @@ -23,78 +23,48 @@ // APIs or functionality, as of now there are derived from concept crate but // we will create a module which will have common trait for event and field which will be used by both event and field as a super trait and // for this we need to create marker trait for event. +// +// Note: Field get/set on the consumer side are modelled as MethodCaller-based callers so +// they reuse the full Method infrastructure (async futures, copy/zero-copy paths, +// MethodReturnSample) instead of a separate field-specific design. +// The interface! macro generates `{name}_get: R::MethodCaller<(), T>` and +// `{name}_set: R::MethodCaller<(T,), T>` fields on the consumer struct, together +// with `get_{name}()` / `set_{name}()` async convenience wrappers. +// On the producer side, FieldPublisher keeps update() + register_set_handler() unchanged. use crate::*; use std::fmt::Debug; -use std::future::Future; -#[allow(dead_code)] -// Temp for build test -// We will remove this once memory layout of same created in rust side like SamplePtr. -#[repr(C)] -#[derive(Debug)] -pub struct MethodReturnTypePtr { - pub value: T, - pub status: Result<()>, -} - -/// FieldSubscriber trait is used to subscribe to a field and get the value of the field. -/// It provides the `get` and `set` methods to get and set the value of the field. -/// It derived from `concept::Subscriber` trait which provides the `subscribe` method to create a field subscription. -/// The `get` and `set` methods for the field instance can be used before subscription. -/// Event related APIs follow the same restriction for concurrent access. +/// `FieldSubscriber` is used to subscribe to a field and receive update notifications. +/// It derives from `concept::Subscriber` which provides the `subscribe()` method to +/// create a `FieldSubscription`. +/// +/// Field get/set operations are NOT part of this trait. They are exposed on the consumer +/// struct as separate async method callers (`get_{name}()` / `set_{name}()`) generated by +/// the `interface!` macro, reusing the Method infrastructure. pub trait FieldSubscriber: concept::Subscriber> { - /// Get the current value of the field. - /// - /// #returns - /// Return the result of `MethodReturnTypePtr` which contains the current value of the field. - /// Note: Get Method before subscription is synchronous and after subscription it is asynchronous. - /// It is because subscribe API take the consumer instance by value and if we provide async get method then it will create issue with subscription. - fn get(&self) -> Result>; - - /// Set the value of the field. - /// - /// # Parameters - /// * `value` - The value to set for the field. - /// - /// # Returns - /// Return the result of `MethodReturnTypePtr` which contains the status of the set operation. - /// with the current value of the field. - fn set(&self, value: &T) -> Result>; } -/// FieldSubscription trait provides the receiving APIs for the field subscription and -/// it is derived from `concept::Subscription` trait which provides the receiving APIs for the field subscription. -/// Additional methods which the field subscription provides are added in this trait. +/// `FieldSubscription` provides the data-receiving APIs for an active field subscription +/// (after `FieldSubscriber::subscribe()` has been called). It derives from +/// `concept::Subscription` which provides `try_receive`, `receive`, +/// `cancellable_receive`, and `to_stream`. +/// +/// In addition to the base subscription APIs, a field subscription exposes: +/// - `get_num_new_samples_available()` — how many fresh samples are ready to receive. +/// - `get_free_sample_count()` — remaining capacity in the subscription buffer. pub trait FieldSubscription: concept::Subscription { - /// Returns the number of new samples a call to try_receive (given parameter max_num_samples - /// doesn't restrict it) would currently provide. - /// How many new sample available for the user of this field subscription to receive. + /// Returns the number of new samples a call to `try_receive` (given that + /// `max_num_samples` does not restrict it) would currently provide. fn get_num_new_samples_available(&self) -> Result; - /// Get the number of samples that can still be received by the user of this field. - /// This is for checking the capacity of the field subscription and to avoid overflow of the field subscription limit. + /// Returns the number of sample slots that can still be filled before the subscription + /// buffer overflows. fn get_free_sample_count(&self) -> Result; - - /// Get the current value of the field. - /// - /// #returns - /// Return the `Future>>` which contains the current value of the field. - fn get(&self) -> impl Future>> + Send; - - ///Set the value of the field. - /// - /// # Parameters - /// * `value` - The value to set for the field. - /// - /// # Returns - /// Return the result of `MethodReturnTypePtr` which contains the status of the set operation. - /// with the current value of the field. - fn set(&self, value: &T) -> Result>; } /// FieldPublisher trait is used to publish a field and update the value of the field. diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_macros.rs index deb55c6ce..f14353a1a 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -467,7 +467,12 @@ macro_rules! interface_consumer_mixed { pub $ev_name: R::Subscriber<$ev_type>, )* $( + // Notification subscriber (subscribe / try_receive / stream). pub $fi_name: R::FieldSubscriber<$fi_type>, + // Async get caller – reuses Method infrastructure (invoke_with_copy(())). + pub [<$fi_name _get>]: R::MethodCaller<(), $fi_type>, + // Async set caller – reuses Method infrastructure (invoke_with_copy((val,))). + pub [<$fi_name _set>]: R::MethodCaller<($fi_type,), $fi_type>, )* $( pub $me_name: R::MethodCaller<($($me_arg_ty,)*), $me_ret>, @@ -494,6 +499,22 @@ macro_rules! interface_consumer_mixed { "Failed to create field subscriber for {}", stringify!($fi_name) )), + [<$fi_name _get>]: + as score_com::MethodCaller<(), $fi_type, R>>::new( + concat!(stringify!($fi_name), "_get"), + instance_info.clone() + ).expect(&format!( + "Failed to create field get caller for {}", + stringify!($fi_name) + )), + [<$fi_name _set>]: + as score_com::MethodCaller<($fi_type,), $fi_type, R>>::new( + concat!(stringify!($fi_name), "_set"), + instance_info.clone() + ).expect(&format!( + "Failed to create field set caller for {}", + stringify!($fi_name) + )), )* $( $me_name: @@ -514,10 +535,36 @@ macro_rules! interface_consumer_mixed { // so copy and zero-copy paths share the same call site. // copy: consumer.method_name(val).await // zero-copy: consumer.method_name(ptr).await + // + // Async field get/set wrappers - one pair per field member. + // consumer.get_field_name().await - async get via MethodCaller<(), T> + // consumer.set_field_name(val).await - async set via MethodCaller<(T,), T> + // These are independent of subscribe() so they work before and after subscription. impl [<$id Consumer>] { $( $crate::_gen_method_wrapper!($me_name ($($me_arg_ty),*) -> $me_ret); )* + $( + /// Asynchronously get the current value of the `$fi_name` field. + /// Returns a future that resolves to `Result>`. + /// Available before and after `subscribe()` - independent of subscription lifecycle. + pub fn []<'a>( + &'a self, + ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _get>], ()) + } + + /// Asynchronously set the value of the `$fi_name` field. + /// Returns a future that resolves to `Result>` + /// containing the confirmed field value from the producer. + /// Available before and after `subscribe()` - independent of subscription lifecycle. + pub fn []<'a>( + &'a self, + value: $fi_type, + ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _set>], (value,)) + } + )* } } }; From 28ce074603107ad42bb2a0e6021a659cf7a2d8f8 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Thu, 30 Jul 2026 14:31:22 +0530 Subject: [PATCH 18/25] Rust::com Add the Field Tags to enable different feature * Added Getter, Setter, Notifier tag for field feature to enable --- .../com-api-gen/com_api_gen.rs | 8 +- .../rust/com-api/com-api-runtime-lola/lib.rs | 4 +- .../com-api/com-api-runtime-lola/method.rs | 88 +++ .../com-api/com-api-runtime-lola/runtime.rs | 6 +- .../com-api/com-api-runtime-mock/runtime.rs | 2 + score/mw/com/rust/score_com.rs | 2 +- .../mw/com/rust/score_com_concept/concept.rs | 12 + .../score_com_concept/interface_macros.rs | 523 +++++++++++++++--- score/mw/com/rust/score_com_concept/lib.rs | 2 +- 9 files changed, 560 insertions(+), 87 deletions(-) diff --git a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs index 2a627d5c8..aed2e73db 100644 --- a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs +++ b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs @@ -79,8 +79,8 @@ interface!( interface!( interface VehicleField { Id = "VehicleFieldInterface", - left_tire: Field, - exhaust: Field, + left_tire: Field, + exhaust: Field, } ); @@ -90,8 +90,8 @@ interface!( Id = "VehicleMonitorInterface", left_tire: Event, exhaust: Event, - left_tire_field: Field, - exhaust_field: Field, + left_tire_field: Field, + exhaust_field: Field, update_tire_pressure(Tire) -> (), update_front_tires_pressure(Tire, Tire) -> (), get_tire_pressure() -> Tire, diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs index b2500d02d..0ea777c35 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs @@ -46,6 +46,6 @@ pub use producer::{ pub use runtime::{LolaRuntimeImpl, RuntimeBuilderImpl}; pub use method::{ - LolaMethodCaller, LolaMethodHandler, LolaMethodInArgAllocator, LolaMethodInArgMaybeUninit, - LolaMethodReturnSample, + LolaFieldGetCaller, LolaFieldSetCaller, LolaMethodCaller, LolaMethodHandler, + LolaMethodInArgAllocator, LolaMethodInArgMaybeUninit, LolaMethodReturnSample, }; diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs index ad1cbf1e1..4f6aefc8c 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs @@ -17,6 +17,7 @@ /// All the struct and trait implementations are placeholders for now, /// and will be implemented in future as per the requirements of the Lola runtime. +use core::fmt::Debug; use core::future::Future; use core::ops::Deref; use score_com_concept::{ @@ -149,3 +150,90 @@ impl MethodInArgAllocator for LolaMethodInArgAllocator { todo!("Implement allocation from the Lola shared-memory region via &self context"); } } + +/// Placeholder caller for field Get operations +/// It is distinct from `LolaMethodCaller<(), T>` because we may need to route field specific Method +/// via specific id file `Getter` to the Lola binding. +pub struct LolaFieldGetCaller { + _phantom: core::marker::PhantomData<(T, R)>, +} + +impl MethodCaller<(), T, R> for LolaFieldGetCaller { + fn new(_method_name: &str, _instance_info: R::ConsumerInfo) -> Result + where + Self: Sized, + { + Ok(LolaFieldGetCaller { + _phantom: core::marker::PhantomData, + }) + } + + fn invoke_with_copy<'a>( + &'a self, + _args: (), + ) -> impl Future>> + 'a { + async move { todo!("Implement field get via MethodType::kGet") } + } + + fn allocate(&self) -> Result<<() as MethodArgsAllocate>::UninitTuple> + where + (): MethodArgsAllocate, + { + todo!("Implement allocate for LolaFieldGetCaller") + } + + fn invoke_zero_copy<'a>( + &'a self, + _ptrs: <() as MethodArgsPtrTuple>::PtrTuple, + ) -> impl Future>> + 'a + where + (): MethodArgsPtrTuple, + { + async move { todo!("Implement zero-copy invoke for LolaFieldGetCaller if C++ side support is available") } + } +} + +/// Placeholder caller for field Set operations +/// It is distinct from `LolaMethodCaller<(T,), T>` because we may need to route field specific Method +/// via specific id file `Setter` to the Lola binding. +pub struct LolaFieldSetCaller { + _phantom: core::marker::PhantomData<(T, R)>, +} + +impl MethodCaller<(T,), T, R> for LolaFieldSetCaller { + fn new(_method_name: &str, _instance_info: R::ConsumerInfo) -> Result + where + Self: Sized, + { + Ok(LolaFieldSetCaller { + _phantom: core::marker::PhantomData, + }) + } + + fn invoke_with_copy<'a>( + &'a self, + _args: (T,), + ) -> impl Future>> + 'a { + async move { todo!("Implement field set via MethodType::kSet") } + } + + fn allocate( + &self, + ) -> Result<<(T,) as MethodArgsAllocate>::UninitTuple> + where + (T,): MethodArgsAllocate, + { + todo!("Implement allocate for LolaFieldSetCaller if C++ side support is available"); + } + + fn invoke_zero_copy<'a>( + &'a self, + _ptrs: <(T,) as MethodArgsPtrTuple>::PtrTuple, + ) -> impl Future>> + 'a + where + (T,): MethodArgsPtrTuple, + { + async move { todo!("Implement zero-copy invoke for LolaFieldSetCaller if C++ side support is available") } + } +} + diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs index 1ea8edb58..0cc67c93c 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs @@ -16,8 +16,8 @@ use core::marker::PhantomData; use std::path::{Path, PathBuf}; use crate::{ - LolaConsumerDiscovery, LolaConsumerInfo, LolaFieldPublisher, LolaFieldSubscriber, - LolaMethodCaller, LolaMethodHandler, + LolaConsumerDiscovery, LolaConsumerInfo, LolaFieldGetCaller, LolaFieldPublisher, + LolaFieldSetCaller, LolaFieldSubscriber, LolaMethodCaller, LolaMethodHandler, LolaMethodInArgAllocator, LolaMethodReturnSample, LolaProducerBuilder, LolaProviderInfo, LolaPublisher, LolaSubscribableImpl, }; @@ -44,6 +44,8 @@ impl Runtime for LolaRuntimeImpl { type MethodHandler = LolaMethodHandler; type FieldPublisher = LolaFieldPublisher; type FieldSubscriber = LolaFieldSubscriber; + type FieldGetCaller = LolaFieldGetCaller; + type FieldSetCaller = LolaFieldSetCaller; type ProviderInfo = LolaProviderInfo; type ConsumerInfo = LolaConsumerInfo; diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs index d6a95b8c8..1b06711a3 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs @@ -78,6 +78,8 @@ impl Runtime for MockRuntimeImpl { type MethodCaller = MockMethodCaller; type MethodHandler = MockMethodHandler; type FieldSubscriber = MockFieldSubscriber; + type FieldGetCaller = MockMethodCaller<(), T, Self>; + type FieldSetCaller = MockMethodCaller<(T,), T, Self>; type FieldPublisher = MockFieldPublisher; type ProviderInfo = MockProviderInfo; type ConsumerInfo = MockConsumerInfo; diff --git a/score/mw/com/rust/score_com.rs b/score/mw/com/rust/score_com.rs index 1aeb50bf5..06c8439f1 100644 --- a/score/mw/com/rust/score_com.rs +++ b/score/mw/com/rust/score_com.rs @@ -149,7 +149,7 @@ pub use score_com_concept::{ MethodInArgAllocator, MethodInArgMaybeUninit, MethodInArgPtr, MethodReturnSample, OfferedProducer, PlacementDefault, Producer, ProducerBuilder, ProviderInfo, Publisher, Reloc, Result, Runtime, RuntimeBuilder, SampleContainer, SampleMaybeUninit, ServiceDiscovery, - Subscriber, Subscription, Uninit, ZeroCopyArgs, + Subscriber, Subscription, Uninit, WithGetter, WithNotifier, WithSetter, ZeroCopyArgs, }; #[doc(hidden)] diff --git a/score/mw/com/rust/score_com_concept/concept.rs b/score/mw/com/rust/score_com_concept/concept.rs index 6ce9a4ad4..bec7f264e 100644 --- a/score/mw/com/rust/score_com_concept/concept.rs +++ b/score/mw/com/rust/score_com_concept/concept.rs @@ -119,6 +119,18 @@ pub trait Runtime { /// `FieldSubscriber` types for Manages subscriptions to field instance type FieldSubscriber: FieldSubscriber; + // Note: below GATs are to make runtime implementation simpler for Field Methods, + // If at the time of implementation no specific need is found for these GATs, + // we can remove them and use MethodCaller instead of a separate field-specific design. + + /// `FieldGetCaller` types for the consumer-side async field get operation. + /// Distinct from `MethodCaller<(), T>` so runtimes can route to specific `Getter`. + type FieldGetCaller: MethodCaller<(), T, Self>; + + /// `FieldSetCaller` types for the consumer-side async field set operation. + /// Distinct from `MethodCaller<(T,), T>` so runtimes can route to specific `Setter`. + type FieldSetCaller: MethodCaller<(T,), T, Self>; + /// `FieldPublisher` types for Publishes field constructs and update the data type FieldPublisher: FieldPublisher; diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_macros.rs index f14353a1a..a2a436606 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -34,6 +34,24 @@ pub struct HandlerNotSet; #[allow(dead_code)] pub struct HandlerSet; +/// Field capability tag: by adding this on interface macro, consumer can call async `get_*()` on this field. +/// Use in `Field` (or combined: `Field`). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct WithGetter; + +/// Field capability tag: by adding this on interface macro, consumer can call async `set_*()` on this field. +/// Use in `Field` (or combined: `Field`). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct WithSetter; + +/// Field capability tag: by adding this on interface macro, consumer can `subscribe()` to field-value-change notifications. +/// Use in `Field` (or combined: `Field`). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct WithNotifier; + /// Main interface macro that generates Consumer, Producer, and OfferedProducer types /// along with all necessary trait implementations. /// @@ -94,7 +112,7 @@ pub struct HandlerSet; /// interface Vehicle { /// Id = "AbcInterface", /// left_tire: Event, -/// left_tire_field: Field, +/// left_tire_field: Field, /// left_tire_method(Tire) -> Tire, /// } /// ); @@ -174,6 +192,9 @@ macro_rules! interface { @id[$id, $uid] @ev[] @fi[] + @fi_n[] + @fi_g[] + @fi_s[] @me[] $($members)* ); @@ -186,30 +207,163 @@ macro_rules! interface { @id[$id, concat!(module_path!(), "::", stringify!($id))] @ev[] @fi[] + @fi_n[] + @fi_g[] + @fi_s[] @me[] $($members)* ); }; } +/// Helper for `_interface_collect_members!`. +/// +/// Iterates over the tag list of a single field, adding the field to the correct per-tag +/// accumulator list. When all tags are consumed, calls back to `_interface_collect_members!` +/// with the updated lists and the remaining interface members. +#[doc(hidden)] +#[macro_export] +macro_rules! _field_split_tags { + // Base: all tags consumed - call back to _interface_collect_members! with updated lists + ( + @ctx[ + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + @rest[$($rest:tt)*] + ] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] + @field[$_name:ident : $_t:ty] + @tags[] + ) => { + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)*] + @fi[$($fi_name : $fi_type ,)*] + @fi_n[$($fin_name : $fin_type ,)*] + @fi_g[$($fig_name : $fig_type ,)*] + @fi_s[$($fis_name : $fis_type ,)*] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + $($rest)* + ); + }; + + // WithNotifier: add field to fi_n list, recurse with remaining tags + ( + @ctx[$($ctx:tt)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fi_g:tt)*] + @fi_s[$($fi_s:tt)*] + @field[$name:ident : $t:ty] + @tags[WithNotifier $(, $rest_tag:ident)*] + ) => { + $crate::_field_split_tags!( + @ctx[$($ctx)*] + @fi_n[$($fin_name : $fin_type ,)* $name : $t ,] + @fi_g[$($fi_g)*] + @fi_s[$($fi_s)*] + @field[$name : $t] + @tags[$($rest_tag),*] + ); + }; + + // WithGetter: add field to fi_g list, recurse with remaining tags + ( + @ctx[$($ctx:tt)*] + @fi_n[$($fi_n:tt)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fi_s:tt)*] + @field[$name:ident : $t:ty] + @tags[WithGetter $(, $rest_tag:ident)*] + ) => { + $crate::_field_split_tags!( + @ctx[$($ctx)*] + @fi_n[$($fi_n)*] + @fi_g[$($fig_name : $fig_type ,)* $name : $t ,] + @fi_s[$($fi_s)*] + @field[$name : $t] + @tags[$($rest_tag),*] + ); + }; + + // WithSetter: add field to fi_s list, recurse with remaining tags + ( + @ctx[$($ctx:tt)*] + @fi_n[$($fi_n:tt)*] + @fi_g[$($fi_g:tt)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] + @field[$name:ident : $t:ty] + @tags[WithSetter $(, $rest_tag:ident)*] + ) => { + $crate::_field_split_tags!( + @ctx[$($ctx)*] + @fi_n[$($fi_n)*] + @fi_g[$($fi_g)*] + @fi_s[$($fis_name : $fis_type ,)* $name : $t ,] + @field[$name : $t] + @tags[$($rest_tag),*] + ); + }; + + // Unrecognized tag + ( + @ctx[$($ctx:tt)*] + @fi_n[$($fi_n:tt)*] + @fi_g[$($fi_g:tt)*] + @fi_s[$($fi_s:tt)*] + @field[$name:ident : $_t:ty] + @tags[$unknown:ident $(, $rest_tag:ident)*] + ) => { + compile_error!(concat!( + "interface!: unrecognized field tag `", + stringify!($unknown), + "` on field `", + stringify!($name), + "`. Supported tags: WithGetter, WithSetter, WithNotifier." + )); + }; +} + /// Internal recursive-macro helper for `interface!`. /// -/// Accumulates members into three typed lists, then calls the mixed generator macros. +/// Accumulates members into typed lists, then calls the mixed generator macros. +/// Field members MUST carry at least one capability tag: `Field`. +/// `Field` without tags is a compile error. +/// Tags control which consumer-side infrastructure is generated per field: +/// - `WithGetter` - `{name}_get: R::FieldGetCaller` + `get_{name}()` async wrapper +/// - `WithSetter` - `{name}_set: R::FieldSetCaller` + `set_{name}(val)` async wrapper +/// - `WithNotifier` - `{name}: R::FieldSubscriber` (subscribe / notifications) +/// Any combination and any ordering of tags is supported. +/// +/// Fields are split into three flat lists during accumulation: +/// @fi_n - fields with WithNotifier tag (name:type) +/// @fi_g - fields with WithGetter tag (name:type) +/// @fi_s - fields with WithSetter tag (name:type) +/// A field with multiple tags appears in multiple lists. +/// The plain @fi list (name:type only) is still kept for forwarding to interface_producer_mixed!. #[doc(hidden)] #[macro_export] macro_rules! _interface_collect_members { - // Base case: nothing left - emit the mixed consumer and producer + // Base case: nothing left - emit the mixed consumer and producer. ( @id[$id:ident, $uid:expr] @ev[$($ev_name:ident : $ev_type:ty ,)*] @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] $(,)? ) => { $crate::interface_consumer_mixed!( $id, events[$($ev_name : $ev_type ,)*], - fields[$($fi_name : $fi_type ,)*], + fields_notifier[$($fin_name : $fin_type ,)*], + fields_getter[$($fig_name : $fig_type ,)*], + fields_setter[$($fis_name : $fis_type ,)*], methods[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] ); $crate::interface_producer_mixed!( @@ -225,6 +379,9 @@ macro_rules! _interface_collect_members { @id[$id:ident, $uid:expr] @ev[$($ev_name:ident : $ev_type:ty ,)*] @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] $name:ident : Event<$t:ty> $(, $($rest:tt)*)? @@ -233,36 +390,80 @@ macro_rules! _interface_collect_members { @id[$id, $uid] @ev[$($ev_name : $ev_type ,)* $name : $t ,] @fi[$($fi_name : $fi_type ,)*] + @fi_n[$($fin_name : $fin_type ,)*] + @fi_g[$($fig_name : $fig_type ,)*] + @fi_s[$($fis_name : $fis_type ,)*] @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] $($($rest)*)? ); }; - // Field member: `name : Field ,?` + // Field member WITH tags: `name : Field ,?` + // Delegates to _field_split_tags! to distribute the field into the per-tag flat lists. ( @id[$id:ident, $uid:expr] @ev[$($ev_name:ident : $ev_type:ty ,)*] @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] - $name:ident : Field<$t:ty> + $name:ident : Field<$t:ty, $first_tag:ident $(+ $rest_tag:ident)*> $(, $($rest:tt)*)? ) => { - $crate::_interface_collect_members!( - @id[$id, $uid] - @ev[$($ev_name : $ev_type ,)*] - @fi[$($fi_name : $fi_type ,)* $name : $t ,] - @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] - $($($rest)*)? + $crate::_field_split_tags!( + @ctx[ + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)*] + @fi[$($fi_name : $fi_type ,)* $name : $t ,] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + @rest[$($($rest)*)?] + ] + @fi_n[$($fin_name : $fin_type ,)*] + @fi_g[$($fig_name : $fig_type ,)*] + @fi_s[$($fis_name : $fis_type ,)*] + @field[$name : $t] + @tags[$first_tag $(, $rest_tag)*] ); }; + // Field member WITHOUT tags: `name : Field ,?` — compile error. + ( + @id[$_id:ident, $_uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $name:ident : Field<$_t:ty> + $(, $($rest:tt)*)? + ) => { + compile_error!(concat!( + "interface!: Field member `", + stringify!($name), + "` requires at least one capability tag.\n", + "Supported tags: WithGetter, WithSetter, WithNotifier\n", + "Use the `+` syntax to combine tags, e.g.:\n", + " ", stringify!($name), ": Field\n", + " ", stringify!($name), ": Field\n", + " ", stringify!($name), ": Field\n", + " ", stringify!($name), ": Field\n", + "Tags control which consumer-side infrastructure is generated:\n", + " WithGetter - get_*()\n", + " WithSetter - set_*()\n", + " WithNotifier - subscribe()\n", + )); + }; + // Method member (fn-like syntax): `name(Arg0, Arg1, ...) -> Ret ,?` - // Positional types - no tuple wrapper needed at the user level. - // Internally stored as a bracketed list: name [Arg0, Arg1, ...] -> Ret ( @id[$id:ident, $uid:expr] @ev[$($ev_name:ident : $ev_type:ty ,)*] @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] $name:ident ( $($arg_ty:ty),* ) -> $ret:ty $(, $($rest:tt)*)? @@ -271,6 +472,9 @@ macro_rules! _interface_collect_members { @id[$id, $uid] @ev[$($ev_name : $ev_type ,)*] @fi[$($fi_name : $fi_type ,)*] + @fi_n[$($fin_name : $fin_type ,)*] + @fi_g[$($fig_name : $fig_type ,)*] + @fi_s[$($fis_name : $fis_type ,)*] @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)* $name [$($arg_ty),*] -> $ret ,] $($($rest)*)? ); @@ -281,6 +485,9 @@ macro_rules! _interface_collect_members { @id[$_id:ident, $_uid:expr] @ev[$($ev_name:ident : $ev_type:ty ,)*] @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] $($unknown:tt)+ ) => { @@ -289,16 +496,16 @@ macro_rules! _interface_collect_members { stringify!($($unknown)+), "`.\n", "Supported member types:\n", - " name: Event - event subscriber / publisher pair\n", - " name: Field - field subscriber / publisher pair\n", - " name(Arg0, Arg1, ...) -> Ret - method caller / handler pair\n", + " name: Event - event subscriber / publisher pair\n", + " name: Field - field with capability tags\n", + " name(Arg0, Arg1, ...) -> Ret - method caller / handler pair\n", + "Note: Field without tags is not allowed. Specify at least one of:\n", + " WithGetter, WithSetter, WithNotifier\n", "Example:\n", " interface!(interface MyIface {\n", " my_event: Event,\n", - " my_field: Field,\n", + " my_field: Field,\n", " my_method(MyData) -> MyData,\n", - " my_void_method(MyData) -> (),\n", - " my_no_arg_method() -> MyData,\n", " });" )); }; @@ -437,28 +644,188 @@ macro_rules! interface_producer { }; } +/// Per-tag struct field declaration helper for `interface_consumer_mixed!`. +/// +/// Called once per field member; iterates over the tag list and emits one struct field per tag. +/// - `WithNotifier` - `pub $name: R::FieldSubscriber<$type>,` +/// - `WithGetter` - `pub {name}_get: R::FieldGetCaller<$type>,` +/// - `WithSetter` - `pub {name}_set: R::FieldSetCaller<$type>,` +/// Any ordering of tags is supported; unrecognized tags produce a compile error. +/// +/// NOTE: Must be called inside a `score_com::paste::paste! { pub struct ... { HERE } }` block +/// since it uses `[<>]` identifier concatenation. +#[doc(hidden)] +#[macro_export] +macro_rules! _field_consumer_decl { + // Base: no more tags + ($fi_name:ident, $fi_type:ty, []) => {}; + + // WithNotifier: emit FieldSubscriber field, recurse + ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*]) => { + pub $fi_name: R::FieldSubscriber<$fi_type>, + $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); + }; + + // WithGetter: emit FieldGetCaller field + ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*]) => { + pub [<$fi_name _get>]: R::FieldGetCaller<$fi_type>, + $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); + }; + + // WithSetter: emit FieldSetCaller field + ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*]) => { + pub [<$fi_name _set>]: R::FieldSetCaller<$fi_type>, + $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); + }; + + // Unrecognized tag + ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*]) => { + compile_error!(concat!( + "interface!: unrecognized field tag `", + stringify!($unknown), + "`. Supported tags: WithGetter, WithSetter, WithNotifier." + )); + }; +} + +/// Per-tag struct field initializer helper for `interface_consumer_mixed!`. +/// +/// Emits one initializer expression per tag (used inside the `Consumer::new()` struct literal). +/// - `WithNotifier` - `$name: R::FieldSubscriber::new(...)` +/// - `WithGetter` - `{name}_get: as MethodCaller<(), T, R>>::new(...)` +/// - `WithSetter` - `{name}_set: as MethodCaller<(T,), T, R>>::new(...)` +/// +/// NOTE: Must be called inside a `score_com::paste::paste! { Struct { HERE } }` block. +#[doc(hidden)] +#[macro_export] +macro_rules! _field_consumer_init { + // Base: no more tags + ($fi_name:ident, $fi_type:ty, [], $instance_info:ident) => {}; + + // WithNotifier: emit FieldSubscriber init, recurse + ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*], $instance_info:ident) => { + $fi_name: R::FieldSubscriber::new( + stringify!($fi_name), + $instance_info.clone() + ).expect(&format!( + "Failed to create field subscriber for {}", + stringify!($fi_name) + )), + $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); + }; + + // WithGetter: emit FieldGetCaller init + ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*], $instance_info:ident) => { + [<$fi_name _get>]: + as score_com::MethodCaller<(), $fi_type, R>>::new( + concat!(stringify!($fi_name), "_get"), + $instance_info.clone() + ).expect(&format!( + "Failed to create field get caller for {}", + stringify!($fi_name) + )), + $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); + }; + + // WithSetter: emit FieldSetCaller init + ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*], $instance_info:ident) => { + [<$fi_name _set>]: + as score_com::MethodCaller<($fi_type,), $fi_type, R>>::new( + concat!(stringify!($fi_name), "_set"), + $instance_info.clone() + ).expect(&format!( + "Failed to create field set caller for {}", + stringify!($fi_name) + )), + $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); + }; + + // Unrecognized tag (already caught by _field_consumer_decl, but guard here too) + ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*], $instance_info:ident) => { + compile_error!(concat!( + "interface!: unrecognized field tag `", + stringify!($unknown), + "`. Supported tags: WithGetter, WithSetter, WithNotifier." + )); + }; +} + +/// Per-tag wrapper method helper for `interface_consumer_mixed!`. +/// +/// Emits one wrapper method per tag inside the `impl {Id}Consumer` block. +/// - `WithNotifier` - no method generated (subscribe is called directly on the struct field) +/// - `WithGetter` - `pub fn get_{name}<'a>(&'a self) -> impl Future> + 'a` +/// - `WithSetter` - `pub fn set_{name}<'a>(&'a self, value: T) -> impl Future> + 'a` +/// +/// NOTE: Must be called inside a `score_com::paste::paste! { impl ... { HERE } }` block. +#[doc(hidden)] +#[macro_export] +macro_rules! _field_consumer_methods { + // Base: no more tags + ($fi_name:ident, $fi_type:ty, []) => {}; + + // WithNotifier: no method generated - subscribe is on the struct field directly + ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*]) => { + $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); + }; + + // WithGetter: emit get_{name}() async wrapper (uses [<>] - must be inside paste!) + ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*]) => { + /// Asynchronously get the current value of the field. + /// Returns a future that resolves to `Result>`. + /// Independent of subscription lifecycle - available before and after `subscribe()`. + pub fn []<'a>( + &'a self, + ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _get>], ()) + } + $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); + }; + + // WithSetter: emit set_{name}(value) async wrapper (uses [<>] - must be inside paste!) + ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*]) => { + /// Asynchronously set the value of the field. + /// Returns a future that resolves to `Result>` + /// containing the confirmed field value from the producer. + /// Independent of subscription lifecycle - available before and after `subscribe()`. + pub fn []<'a>( + &'a self, + value: $fi_type, + ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _set>], (value,)) + } + $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); + }; + + // Unrecognized tag + ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*]) => { + compile_error!(concat!( + "interface!: unrecognized field tag `", + stringify!($unknown), + "`. Supported tags: WithGetter, WithSetter, WithNotifier." + )); + }; +} + /// Generates the `{id}Consumer` struct and its `Consumer` trait implementation for /// interfaces that may contain any combination of events, fields, and methods. /// -/// # Generated struct fields -/// - `pub $ev_name: R::Subscriber<$ev_type>` - one per event -/// - `pub $fi_name: R::FieldSubscriber<$fi_type>` - one per field -/// - `pub $me_name: R::MethodCaller<($me_arg_ty,...), $me_ret>` - one per method +/// Field members are passed as three separate flat lists (one per tag type): +/// - `fields_notifier[name:type,...]` - `pub name: R::FieldSubscriber` (subscribe/notifications) +/// - `fields_getter[name:type,...]` - `pub name_get: R::FieldGetCaller` + `get_name()` wrapper +/// - `fields_setter[name:type,...]` - `pub name_set: R::FieldSetCaller` + `set_name(val)` wrapper /// -/// # method wrappers -/// For each method member a positional-argument `pub fn $me_name(&self, arg0: A0, ...)` wrapper -/// is generated (via `_gen_method_wrapper!`). The wrapper packs the positional args into a tuple -/// and dispatches through `MethodCallInput`, so both copy and zero-copy paths use the same call site. -/// The wrapper returns `impl Future> + '_`. -/// copy: `consumer.method(val).await` - `val: T` - copy path -/// zero-copy: `consumer.method(ptr).await` - `ptr: MethodInArgPtr` - zero-copy path +/// A field with multiple tags (e.g. `WithGetter + WithSetter`) appears in multiple lists. +/// Method members get positional-arg wrapper functions via `_gen_method_wrapper!`. #[doc(hidden)] #[macro_export] macro_rules! interface_consumer_mixed { ( $id:ident, events[$($ev_name:ident : $ev_type:ty ,)*], - fields[$($fi_name:ident : $fi_type:ty ,)*], + fields_notifier[$($fin_name:ident : $fin_type:ty ,)*], + fields_getter[$($fig_name:ident : $fig_type:ty ,)*], + fields_setter[$($fis_name:ident : $fis_type:ty ,)*], methods[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] ) => { score_com::paste::paste! { @@ -467,12 +834,13 @@ macro_rules! interface_consumer_mixed { pub $ev_name: R::Subscriber<$ev_type>, )* $( - // Notification subscriber (subscribe / try_receive / stream). - pub $fi_name: R::FieldSubscriber<$fi_type>, - // Async get caller – reuses Method infrastructure (invoke_with_copy(())). - pub [<$fi_name _get>]: R::MethodCaller<(), $fi_type>, - // Async set caller – reuses Method infrastructure (invoke_with_copy((val,))). - pub [<$fi_name _set>]: R::MethodCaller<($fi_type,), $fi_type>, + pub $fin_name: R::FieldSubscriber<$fin_type>, + )* + $( + pub [<$fig_name _get>]: R::FieldGetCaller<$fig_type>, + )* + $( + pub [<$fis_name _set>]: R::FieldSetCaller<$fis_type>, )* $( pub $me_name: R::MethodCaller<($($me_arg_ty,)*), $me_ret>, @@ -492,28 +860,32 @@ macro_rules! interface_consumer_mixed { )), )* $( - $fi_name: R::FieldSubscriber::new( - stringify!($fi_name), + $fin_name: R::FieldSubscriber::new( + stringify!($fin_name), instance_info.clone() ).expect(&format!( "Failed to create field subscriber for {}", - stringify!($fi_name) + stringify!($fin_name) )), - [<$fi_name _get>]: - as score_com::MethodCaller<(), $fi_type, R>>::new( - concat!(stringify!($fi_name), "_get"), + )* + $( + [<$fig_name _get>]: + as score_com::MethodCaller<(), $fig_type, R>>::new( + concat!(stringify!($fig_name), "_get"), instance_info.clone() ).expect(&format!( "Failed to create field get caller for {}", - stringify!($fi_name) + stringify!($fig_name) )), - [<$fi_name _set>]: - as score_com::MethodCaller<($fi_type,), $fi_type, R>>::new( - concat!(stringify!($fi_name), "_set"), + )* + $( + [<$fis_name _set>]: + as score_com::MethodCaller<($fis_type,), $fis_type, R>>::new( + concat!(stringify!($fis_name), "_set"), instance_info.clone() ).expect(&format!( "Failed to create field set caller for {}", - stringify!($fi_name) + stringify!($fis_name) )), )* $( @@ -530,39 +902,33 @@ macro_rules! interface_consumer_mixed { } } - // Positional-argument convenience wrappers - one per method member. - // The wrapper packs args into a tuple and dispatches via MethodCallInput, - // so copy and zero-copy paths share the same call site. - // copy: consumer.method_name(val).await - // zero-copy: consumer.method_name(ptr).await - // - // Async field get/set wrappers - one pair per field member. - // consumer.get_field_name().await - async get via MethodCaller<(), T> - // consumer.set_field_name(val).await - async set via MethodCaller<(T,), T> - // These are independent of subscribe() so they work before and after subscription. + // Method wrappers: positional-arg convenience functions via MethodCallInput. + // Field get/set wrappers: async get/set via FieldGetCaller/FieldSetCaller. + // subscribe() is available directly on the struct field for WithNotifier fields. impl [<$id Consumer>] { $( $crate::_gen_method_wrapper!($me_name ($($me_arg_ty),*) -> $me_ret); )* $( - /// Asynchronously get the current value of the `$fi_name` field. - /// Returns a future that resolves to `Result>`. - /// Available before and after `subscribe()` - independent of subscription lifecycle. - pub fn []<'a>( + /// Asynchronously get the current value of the field. + /// Returns a future that resolves to `Result>`. + /// Independent of subscription lifecycle. + pub fn []<'a>( &'a self, - ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { - score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _get>], ()) + ) -> impl core::future::Future::MethodReturnSample<$fig_type>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.[<$fig_name _get>], ()) } - - /// Asynchronously set the value of the `$fi_name` field. - /// Returns a future that resolves to `Result>` + )* + $( + /// Asynchronously set the value of the field. + /// Returns a future that resolves to `Result>` /// containing the confirmed field value from the producer. - /// Available before and after `subscribe()` - independent of subscription lifecycle. - pub fn []<'a>( + /// Independent of subscription lifecycle. + pub fn []<'a>( &'a self, - value: $fi_type, - ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { - score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _set>], (value,)) + value: $fis_type, + ) -> impl core::future::Future::MethodReturnSample<$fis_type>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.[<$fis_name _set>], (value,)) } )* } @@ -751,7 +1117,7 @@ macro_rules! _gen_method_wrapper { score_com::MethodCaller::invoke_with_copy(&self.$me_name, ()) } }; - // 1–N args - delegate to the self-counting recursive macro. + // 1-N args - delegate to the self-counting recursive macro. ($me_name:ident ($($t:ty),+) -> $me_ret:ty) => { $crate::_gen_method_wrapper_collect!( $me_name -> $me_ret ; @@ -936,7 +1302,8 @@ mod tests { /// /// ``` /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// WithGetter, WithSetter, WithNotifier}; /// /// #[derive(Debug, Reloc)] /// #[repr(C)] @@ -949,7 +1316,7 @@ mod tests { /// interface Vehicle { /// Id = "AbcInterface", /// left_tire: Event, - /// left_tire_field: Field, + /// left_tire_field: Field, /// left_tire_method(Tire) -> Tire, /// } /// ); @@ -958,7 +1325,9 @@ mod tests { /// Generates `VehicleInterface`, `VehicleConsumer`, `VehicleProducer`, /// and `VehicleOfferedProducer` where: /// - `VehicleConsumer` has `left_tire: Subscriber`, - /// `left_tire_field: FieldSubscriber`, + /// `left_tire_field: FieldSubscriber` (from `WithNotifier`), + /// `left_tire_field_get: FieldGetCaller` (from `WithGetter`), + /// `left_tire_field_set: FieldSetCaller` (from `WithSetter`), /// `left_tire_method: MethodCaller<(Tire,), Tire>`, /// and a convenience `left_tire_method(arg0: Tire)` method. /// - `VehicleProducer` derives `TypeStateValidator` and requires the `.init()` chain: diff --git a/score/mw/com/rust/score_com_concept/lib.rs b/score/mw/com/rust/score_com_concept/lib.rs index 773fee8ca..4976a42f7 100644 --- a/score/mw/com/rust/score_com_concept/lib.rs +++ b/score/mw/com/rust/score_com_concept/lib.rs @@ -30,7 +30,7 @@ mod reloc; pub use concept::*; pub use error::*; pub use field_concept::*; -pub use interface_macros::{HandlerNotSet, HandlerSet, Init, Uninit}; +pub use interface_macros::{HandlerNotSet, HandlerSet, Init, Uninit, WithGetter, WithNotifier, WithSetter}; pub use method_concept::*; #[doc(hidden)] pub use paste; From b8a687eb6bb42bb38e1cd59e75c0af4a9b7c64da Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Thu, 30 Jul 2026 15:26:26 +0530 Subject: [PATCH 19/25] Rust::com Interface macro moduler * Create separate file for Producer and Consumer interface macro --- score/mw/com/rust/score_com_concept/BUILD | 2 +- .../interface_consumer_macros.rs | 477 ++++++++++++++++++ ...macros.rs => interface_producer_macros.rs} | 464 ----------------- score/mw/com/rust/score_com_concept/lib.rs | 5 +- 4 files changed, 481 insertions(+), 467 deletions(-) create mode 100644 score/mw/com/rust/score_com_concept/interface_consumer_macros.rs rename score/mw/com/rust/score_com_concept/{interface_macros.rs => interface_producer_macros.rs} (74%) diff --git a/score/mw/com/rust/score_com_concept/BUILD b/score/mw/com/rust/score_com_concept/BUILD index c7447ef4d..87707bac7 100644 --- a/score/mw/com/rust/score_com_concept/BUILD +++ b/score/mw/com/rust/score_com_concept/BUILD @@ -55,7 +55,7 @@ rust_doc_test( rust_unit_test( name = "score_com_concept-macros-unit-tests", - srcs = ["interface_macros.rs"], + srcs = ["interface_producer_macros.rs"], features = ["link_std_cpp_lib"], # TODO: remove tags = ["manual"] once field or method PR is merged, # Unit test failed because macro has field and method both types diff --git a/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs b/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs new file mode 100644 index 000000000..a7eb8872b --- /dev/null +++ b/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs @@ -0,0 +1,477 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/// Type-state marker for uninitialized field value state (compile-time tracking). +/// +/// These marker types are never constructed as values - they only appear as generic +/// type parameters inside `PhantomData<(S, H)>` on the generated `{Id}Validator` struct +/// (see `TypeStateValidator` in `score_com_macros`). The compiler's `dead_code` lint +/// flags unit structs that are never instantiated, so it is suppressed here deliberately. +#[allow(dead_code)] +pub struct Uninit; + +/// Type-state marker for initialized field value state (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct Init; + +/// Type-state marker for handler not registered (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct HandlerNotSet; + +/// Type-state marker for handler registered (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct HandlerSet; + +/// Field capability tag: by adding this on interface macro, consumer can call async `get_*()` on this field. +/// Use in `Field` (or combined: `Field`). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct WithGetter; + +/// Field capability tag: by adding this on interface macro, consumer can call async `set_*()` on this field. +/// Use in `Field` (or combined: `Field`). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct WithSetter; + +/// Field capability tag: by adding this on interface macro, consumer can `subscribe()` to field-value-change notifications. +/// Use in `Field` (or combined: `Field`). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct WithNotifier; + +/// Macro to implement the Consumer trait for a given interface ID and its events. +/// +/// Generates the Consumer struct with subscribers for each event. +// TODO: This can be removed once verification is done that the new interface_producer_mixed! macro works for event-only interfaces. +#[macro_export] +macro_rules! interface_consumer { + ($id:ident, $($event_name:ident, Event<$event_type:ty>),+$(,)?) => { + score_com::paste::paste! { + pub struct [<$id Consumer>] { + $( + pub $event_name: R::Subscriber<$event_type>, + )+ + } + + impl score_com::Consumer for [<$id Consumer>] { + fn new(instance_info: R::ConsumerInfo) -> Self { + [<$id Consumer>] { + $( + $event_name: R::Subscriber::new( + stringify!($event_name), + instance_info.clone() + ).expect(&format!( + "Failed to create subscriber for {}", + stringify!($event_name) + )), + )+ + } + } + } + } + }; +} + +/// Per-tag struct field declaration helper for `interface_consumer_mixed!`. +/// +/// Called once per field member; iterates over the tag list and emits one struct field per tag. +/// - `WithNotifier` - `pub $name: R::FieldSubscriber<$type>,` +/// - `WithGetter` - `pub {name}_get: R::FieldGetCaller<$type>,` +/// - `WithSetter` - `pub {name}_set: R::FieldSetCaller<$type>,` +/// Any ordering of tags is supported; unrecognized tags produce a compile error. +/// +/// NOTE: Must be called inside a `score_com::paste::paste! { pub struct ... { HERE } }` block +/// since it uses `[<>]` identifier concatenation. +#[doc(hidden)] +#[macro_export] +macro_rules! _field_consumer_decl { + // Base: no more tags + ($fi_name:ident, $fi_type:ty, []) => {}; + + // WithNotifier: emit FieldSubscriber field, recurse + ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*]) => { + pub $fi_name: R::FieldSubscriber<$fi_type>, + $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); + }; + + // WithGetter: emit FieldGetCaller field + ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*]) => { + pub [<$fi_name _get>]: R::FieldGetCaller<$fi_type>, + $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); + }; + + // WithSetter: emit FieldSetCaller field + ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*]) => { + pub [<$fi_name _set>]: R::FieldSetCaller<$fi_type>, + $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); + }; + + // Unrecognized tag + ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*]) => { + compile_error!(concat!( + "interface!: unrecognized field tag `", + stringify!($unknown), + "`. Supported tags: WithGetter, WithSetter, WithNotifier." + )); + }; +} + +/// Per-tag struct field initializer helper for `interface_consumer_mixed!`. +/// +/// Emits one initializer expression per tag (used inside the `Consumer::new()` struct literal). +/// - `WithNotifier` - `$name: R::FieldSubscriber::new(...)` +/// - `WithGetter` - `{name}_get: as MethodCaller<(), T, R>>::new(...)` +/// - `WithSetter` - `{name}_set: as MethodCaller<(T,), T, R>>::new(...)` +/// +/// NOTE: Must be called inside a `score_com::paste::paste! { Struct { HERE } }` block. +#[doc(hidden)] +#[macro_export] +macro_rules! _field_consumer_init { + // Base: no more tags + ($fi_name:ident, $fi_type:ty, [], $instance_info:ident) => {}; + + // WithNotifier: emit FieldSubscriber init, recurse + ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*], $instance_info:ident) => { + $fi_name: R::FieldSubscriber::new( + stringify!($fi_name), + $instance_info.clone() + ).expect(&format!( + "Failed to create field subscriber for {}", + stringify!($fi_name) + )), + $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); + }; + + // WithGetter: emit FieldGetCaller init + ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*], $instance_info:ident) => { + [<$fi_name _get>]: + as score_com::MethodCaller<(), $fi_type, R>>::new( + concat!(stringify!($fi_name), "_get"), + $instance_info.clone() + ).expect(&format!( + "Failed to create field get caller for {}", + stringify!($fi_name) + )), + $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); + }; + + // WithSetter: emit FieldSetCaller init + ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*], $instance_info:ident) => { + [<$fi_name _set>]: + as score_com::MethodCaller<($fi_type,), $fi_type, R>>::new( + concat!(stringify!($fi_name), "_set"), + $instance_info.clone() + ).expect(&format!( + "Failed to create field set caller for {}", + stringify!($fi_name) + )), + $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); + }; + + // Unrecognized tag (already caught by _field_consumer_decl, but guard here too) + ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*], $instance_info:ident) => { + compile_error!(concat!( + "interface!: unrecognized field tag `", + stringify!($unknown), + "`. Supported tags: WithGetter, WithSetter, WithNotifier." + )); + }; +} + +/// Per-tag wrapper method helper for `interface_consumer_mixed!`. +/// +/// Emits one wrapper method per tag inside the `impl {Id}Consumer` block. +/// - `WithNotifier` - no method generated (subscribe is called directly on the struct field) +/// - `WithGetter` - `pub fn get_{name}<'a>(&'a self) -> impl Future> + 'a` +/// - `WithSetter` - `pub fn set_{name}<'a>(&'a self, value: T) -> impl Future> + 'a` +/// +/// NOTE: Must be called inside a `score_com::paste::paste! { impl ... { HERE } }` block. +#[doc(hidden)] +#[macro_export] +macro_rules! _field_consumer_methods { + // Base: no more tags + ($fi_name:ident, $fi_type:ty, []) => {}; + + // WithNotifier: no method generated - subscribe is on the struct field directly + ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*]) => { + $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); + }; + + // WithGetter: emit get_{name}() async wrapper (uses [<>] - must be inside paste!) + ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*]) => { + /// Asynchronously get the current value of the field. + /// Returns a future that resolves to `Result>`. + /// Independent of subscription lifecycle - available before and after `subscribe()`. + pub fn []<'a>( + &'a self, + ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _get>], ()) + } + $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); + }; + + // WithSetter: emit set_{name}(value) async wrapper (uses [<>] - must be inside paste!) + ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*]) => { + /// Asynchronously set the value of the field. + /// Returns a future that resolves to `Result>` + /// containing the confirmed field value from the producer. + /// Independent of subscription lifecycle - available before and after `subscribe()`. + pub fn []<'a>( + &'a self, + value: $fi_type, + ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _set>], (value,)) + } + $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); + }; + + // Unrecognized tag + ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*]) => { + compile_error!(concat!( + "interface!: unrecognized field tag `", + stringify!($unknown), + "`. Supported tags: WithGetter, WithSetter, WithNotifier." + )); + }; +} + +/// Generates the `{id}Consumer` struct and its `Consumer` trait implementation for +/// interfaces that may contain any combination of events, fields, and methods. +/// +/// Field members are passed as three separate flat lists (one per tag type): +/// - `fields_notifier[name:type,...]` - `pub name: R::FieldSubscriber` (subscribe/notifications) +/// - `fields_getter[name:type,...]` - `pub name_get: R::FieldGetCaller` + `get_name()` wrapper +/// - `fields_setter[name:type,...]` - `pub name_set: R::FieldSetCaller` + `set_name(val)` wrapper +/// +/// A field with multiple tags (e.g. `WithGetter + WithSetter`) appears in multiple lists. +/// Method members get positional-arg wrapper functions via `_gen_method_wrapper!`. +#[doc(hidden)] +#[macro_export] +macro_rules! interface_consumer_mixed { + ( + $id:ident, + events[$($ev_name:ident : $ev_type:ty ,)*], + fields_notifier[$($fin_name:ident : $fin_type:ty ,)*], + fields_getter[$($fig_name:ident : $fig_type:ty ,)*], + fields_setter[$($fis_name:ident : $fis_type:ty ,)*], + methods[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + ) => { + score_com::paste::paste! { + pub struct [<$id Consumer>] { + $( + pub $ev_name: R::Subscriber<$ev_type>, + )* + $( + pub $fin_name: R::FieldSubscriber<$fin_type>, + )* + $( + pub [<$fig_name _get>]: R::FieldGetCaller<$fig_type>, + )* + $( + pub [<$fis_name _set>]: R::FieldSetCaller<$fis_type>, + )* + $( + pub $me_name: R::MethodCaller<($($me_arg_ty,)*), $me_ret>, + )* + } + + impl score_com::Consumer for [<$id Consumer>] { + fn new(instance_info: R::ConsumerInfo) -> Self { + [<$id Consumer>] { + $( + $ev_name: R::Subscriber::new( + stringify!($ev_name), + instance_info.clone() + ).expect(&format!( + "Failed to create subscriber for {}", + stringify!($ev_name) + )), + )* + $( + $fin_name: R::FieldSubscriber::new( + stringify!($fin_name), + instance_info.clone() + ).expect(&format!( + "Failed to create field subscriber for {}", + stringify!($fin_name) + )), + )* + $( + [<$fig_name _get>]: + as score_com::MethodCaller<(), $fig_type, R>>::new( + concat!(stringify!($fig_name), "_get"), + instance_info.clone() + ).expect(&format!( + "Failed to create field get caller for {}", + stringify!($fig_name) + )), + )* + $( + [<$fis_name _set>]: + as score_com::MethodCaller<($fis_type,), $fis_type, R>>::new( + concat!(stringify!($fis_name), "_set"), + instance_info.clone() + ).expect(&format!( + "Failed to create field set caller for {}", + stringify!($fis_name) + )), + )* + $( + $me_name: + as score_com::MethodCaller<($($me_arg_ty,)*), $me_ret, R>>::new( + stringify!($me_name), + instance_info.clone() + ).expect(&format!( + "Failed to create method caller for {}", + stringify!($me_name) + )), + )* + } + } + } + + // Method wrappers: positional-arg convenience functions via MethodCallInput. + // Field get/set wrappers: async get/set via FieldGetCaller/FieldSetCaller. + // subscribe() is available directly on the struct field for WithNotifier fields. + impl [<$id Consumer>] { + $( + $crate::_gen_method_wrapper!($me_name ($($me_arg_ty),*) -> $me_ret); + )* + $( + /// Asynchronously get the current value of the field. + /// Returns a future that resolves to `Result>`. + /// Independent of subscription lifecycle. + pub fn []<'a>( + &'a self, + ) -> impl core::future::Future::MethodReturnSample<$fig_type>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.[<$fig_name _get>], ()) + } + )* + $( + /// Asynchronously set the value of the field. + /// Returns a future that resolves to `Result>` + /// containing the confirmed field value from the producer. + /// Independent of subscription lifecycle. + pub fn []<'a>( + &'a self, + value: $fis_type, + ) -> impl core::future::Future::MethodReturnSample<$fis_type>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.[<$fis_name _set>], (value,)) + } + )* + } + } + }; +} + +/// Entry-point wrapper generator. +/// Every generated wrapper returns `impl Future>> + '_`. +/// +/// # Generated call sites +/// ```text +/// consumer.method(val).await - copy path - val: ArgType +/// consumer.method(ptr).await - zero-copy - ptr: MethodInArgPtr +/// ``` +#[doc(hidden)] +#[macro_export] +macro_rules! _gen_method_wrapper { + // 0 args - invoke_with_copy directly; no zero-copy path (nothing to allocate). + // This is for kind of `get` methods that take no arguments and return a value. + ($me_name:ident () -> $me_ret:ty) => { + pub fn $me_name<'a>(&'a self) -> impl core::future::Future::MethodReturnSample<$me_ret>>> + 'a { + score_com::MethodCaller::invoke_with_copy(&self.$me_name, ()) + } + }; + // 1-N args - delegate to the self-counting recursive macro. + ($me_name:ident ($($t:ty),+) -> $me_ret:ty) => { + $crate::_gen_method_wrapper_collect!( + $me_name -> $me_ret ; + @counter[] + @acc[] + @types[$($t),+] + ); + }; +} + +/// Recursive macro for `_gen_method_wrapper!`. +/// +/// Self-counting: instead of zipping the method's positional type list against a +/// pre-defined pool of `(arg_name, generic_name)` identifiers, this recursive macro synthesizes +/// a fresh, unique `(argN : _AN : TypeN)` triplet at each recursion step directly from a +/// growing counter of `n` marker tokens (via `paste!`), then calls +/// `_gen_method_wrapper_body!` once the type list is exhausted. +/// +/// This mirrors the self-contained recursion used by `impl_all_arities!` in +/// `method_arities.rs`: there is no separate pool to keep in sync, and no fixed +/// argument-count limit - any arity supported by `method_arities.rs` works automatically. +#[doc(hidden)] +#[macro_export] +macro_rules! _gen_method_wrapper_collect { + // Base: all types consumed - emit the function via the body macro. + ( + $me_name:ident -> $me_ret:ty ; + @counter[$($n:tt)*] + @acc[$($acc:tt),*] + @types[] + ) => { + $crate::_gen_method_wrapper_body!($me_name -> $me_ret ; [$($acc),*]); + }; + + // Step: consume one type, grow the counter by one `n`, and synthesize a fresh + // (param, generic) identifier pair from the counter via `paste!`. + ( + $me_name:ident -> $me_ret:ty ; + @counter[$($n:tt)*] + @acc[$($acc:tt),*] + @types[$t:ty $(, $rest_t:ty)*] + ) => { + score_com::paste::paste! { + $crate::_gen_method_wrapper_collect!( + $me_name -> $me_ret ; + @counter[$($n)* n] + @acc[$($acc,)* ([] : [<_A $($n)*>] : $t)] + @types[$($rest_t),*] + ); + } + }; +} + +/// Generates the wrapper function from an accumulated list of `(argN : _AN : TypeN)`. +/// +/// This generates a wrapper function template. +/// All arities use this one arm - the function body is written once, not duplicated per arity. +/// Called by `_gen_method_wrapper_collect!` after it has built the full triplet list. +/// +/// The generated function returns `impl Future>> + 'a` so callers +/// can `.await` the method call, e.g. `consumer.method_name(arg0).await?`. +#[doc(hidden)] +#[macro_export] +macro_rules! _gen_method_wrapper_body { + ($me_name:ident -> $me_ret:ty ; [$(($p:ident : $g:ident : $c:ty)),+]) => { + pub fn $me_name<'a, $($g),+>( + &'a self, + $($p: $g),+ + ) -> impl core::future::Future::MethodReturnSample<$me_ret>>> + 'a + where + ($($g,)+): score_com::MethodCallInput<($($c,)+), $me_ret, R>, + R::MethodCaller<($($c,)+), $me_ret>: + score_com::MethodCaller<($($c,)+), $me_ret, R>, + { + score_com::MethodCallInput::invoke(($($p,)+), &self.$me_name) + } + }; +} diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_producer_macros.rs similarity index 74% rename from score/mw/com/rust/score_com_concept/interface_macros.rs rename to score/mw/com/rust/score_com_concept/interface_producer_macros.rs index a2a436606..3b441c931 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_producer_macros.rs @@ -10,47 +10,6 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/// Type-state marker for uninitialized field value state (compile-time tracking). -/// -/// These marker types are never constructed as values - they only appear as generic -/// type parameters inside `PhantomData<(S, H)>` on the generated `{Id}Validator` struct -/// (see `TypeStateValidator` in `score_com_macros`). The compiler's `dead_code` lint -/// flags unit structs that are never instantiated, so it is suppressed here deliberately. -#[allow(dead_code)] -pub struct Uninit; - -/// Type-state marker for initialized field value state (compile-time tracking). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct Init; - -/// Type-state marker for handler not registered (compile-time tracking). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct HandlerNotSet; - -/// Type-state marker for handler registered (compile-time tracking). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct HandlerSet; - -/// Field capability tag: by adding this on interface macro, consumer can call async `get_*()` on this field. -/// Use in `Field` (or combined: `Field`). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct WithGetter; - -/// Field capability tag: by adding this on interface macro, consumer can call async `set_*()` on this field. -/// Use in `Field` (or combined: `Field`). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct WithSetter; - -/// Field capability tag: by adding this on interface macro, consumer can `subscribe()` to field-value-change notifications. -/// Use in `Field` (or combined: `Field`). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct WithNotifier; /// Main interface macro that generates Consumer, Producer, and OfferedProducer types /// along with all necessary trait implementations. @@ -543,39 +502,6 @@ macro_rules! interface_common { }; } -/// Macro to implement the Consumer trait for a given interface ID and its events. -/// -/// Generates the Consumer struct with subscribers for each event. -// TODO: This can be removed once verification is done that the new interface_producer_mixed! macro works for event-only interfaces. -#[macro_export] -macro_rules! interface_consumer { - ($id:ident, $($event_name:ident, Event<$event_type:ty>),+$(,)?) => { - score_com::paste::paste! { - pub struct [<$id Consumer>] { - $( - pub $event_name: R::Subscriber<$event_type>, - )+ - } - - impl score_com::Consumer for [<$id Consumer>] { - fn new(instance_info: R::ConsumerInfo) -> Self { - [<$id Consumer>] { - $( - $event_name: R::Subscriber::new( - stringify!($event_name), - instance_info.clone() - ).expect(&format!( - "Failed to create subscriber for {}", - stringify!($event_name) - )), - )+ - } - } - } - } - }; -} - /// This is Event specific. /// Macro to implement the Producer and OfferedProducer traits for /// a given interface ID and its events. @@ -644,298 +570,6 @@ macro_rules! interface_producer { }; } -/// Per-tag struct field declaration helper for `interface_consumer_mixed!`. -/// -/// Called once per field member; iterates over the tag list and emits one struct field per tag. -/// - `WithNotifier` - `pub $name: R::FieldSubscriber<$type>,` -/// - `WithGetter` - `pub {name}_get: R::FieldGetCaller<$type>,` -/// - `WithSetter` - `pub {name}_set: R::FieldSetCaller<$type>,` -/// Any ordering of tags is supported; unrecognized tags produce a compile error. -/// -/// NOTE: Must be called inside a `score_com::paste::paste! { pub struct ... { HERE } }` block -/// since it uses `[<>]` identifier concatenation. -#[doc(hidden)] -#[macro_export] -macro_rules! _field_consumer_decl { - // Base: no more tags - ($fi_name:ident, $fi_type:ty, []) => {}; - - // WithNotifier: emit FieldSubscriber field, recurse - ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*]) => { - pub $fi_name: R::FieldSubscriber<$fi_type>, - $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); - }; - - // WithGetter: emit FieldGetCaller field - ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*]) => { - pub [<$fi_name _get>]: R::FieldGetCaller<$fi_type>, - $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); - }; - - // WithSetter: emit FieldSetCaller field - ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*]) => { - pub [<$fi_name _set>]: R::FieldSetCaller<$fi_type>, - $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); - }; - - // Unrecognized tag - ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*]) => { - compile_error!(concat!( - "interface!: unrecognized field tag `", - stringify!($unknown), - "`. Supported tags: WithGetter, WithSetter, WithNotifier." - )); - }; -} - -/// Per-tag struct field initializer helper for `interface_consumer_mixed!`. -/// -/// Emits one initializer expression per tag (used inside the `Consumer::new()` struct literal). -/// - `WithNotifier` - `$name: R::FieldSubscriber::new(...)` -/// - `WithGetter` - `{name}_get: as MethodCaller<(), T, R>>::new(...)` -/// - `WithSetter` - `{name}_set: as MethodCaller<(T,), T, R>>::new(...)` -/// -/// NOTE: Must be called inside a `score_com::paste::paste! { Struct { HERE } }` block. -#[doc(hidden)] -#[macro_export] -macro_rules! _field_consumer_init { - // Base: no more tags - ($fi_name:ident, $fi_type:ty, [], $instance_info:ident) => {}; - - // WithNotifier: emit FieldSubscriber init, recurse - ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*], $instance_info:ident) => { - $fi_name: R::FieldSubscriber::new( - stringify!($fi_name), - $instance_info.clone() - ).expect(&format!( - "Failed to create field subscriber for {}", - stringify!($fi_name) - )), - $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); - }; - - // WithGetter: emit FieldGetCaller init - ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*], $instance_info:ident) => { - [<$fi_name _get>]: - as score_com::MethodCaller<(), $fi_type, R>>::new( - concat!(stringify!($fi_name), "_get"), - $instance_info.clone() - ).expect(&format!( - "Failed to create field get caller for {}", - stringify!($fi_name) - )), - $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); - }; - - // WithSetter: emit FieldSetCaller init - ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*], $instance_info:ident) => { - [<$fi_name _set>]: - as score_com::MethodCaller<($fi_type,), $fi_type, R>>::new( - concat!(stringify!($fi_name), "_set"), - $instance_info.clone() - ).expect(&format!( - "Failed to create field set caller for {}", - stringify!($fi_name) - )), - $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); - }; - - // Unrecognized tag (already caught by _field_consumer_decl, but guard here too) - ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*], $instance_info:ident) => { - compile_error!(concat!( - "interface!: unrecognized field tag `", - stringify!($unknown), - "`. Supported tags: WithGetter, WithSetter, WithNotifier." - )); - }; -} - -/// Per-tag wrapper method helper for `interface_consumer_mixed!`. -/// -/// Emits one wrapper method per tag inside the `impl {Id}Consumer` block. -/// - `WithNotifier` - no method generated (subscribe is called directly on the struct field) -/// - `WithGetter` - `pub fn get_{name}<'a>(&'a self) -> impl Future> + 'a` -/// - `WithSetter` - `pub fn set_{name}<'a>(&'a self, value: T) -> impl Future> + 'a` -/// -/// NOTE: Must be called inside a `score_com::paste::paste! { impl ... { HERE } }` block. -#[doc(hidden)] -#[macro_export] -macro_rules! _field_consumer_methods { - // Base: no more tags - ($fi_name:ident, $fi_type:ty, []) => {}; - - // WithNotifier: no method generated - subscribe is on the struct field directly - ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*]) => { - $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); - }; - - // WithGetter: emit get_{name}() async wrapper (uses [<>] - must be inside paste!) - ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*]) => { - /// Asynchronously get the current value of the field. - /// Returns a future that resolves to `Result>`. - /// Independent of subscription lifecycle - available before and after `subscribe()`. - pub fn []<'a>( - &'a self, - ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { - score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _get>], ()) - } - $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); - }; - - // WithSetter: emit set_{name}(value) async wrapper (uses [<>] - must be inside paste!) - ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*]) => { - /// Asynchronously set the value of the field. - /// Returns a future that resolves to `Result>` - /// containing the confirmed field value from the producer. - /// Independent of subscription lifecycle - available before and after `subscribe()`. - pub fn []<'a>( - &'a self, - value: $fi_type, - ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { - score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _set>], (value,)) - } - $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); - }; - - // Unrecognized tag - ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*]) => { - compile_error!(concat!( - "interface!: unrecognized field tag `", - stringify!($unknown), - "`. Supported tags: WithGetter, WithSetter, WithNotifier." - )); - }; -} - -/// Generates the `{id}Consumer` struct and its `Consumer` trait implementation for -/// interfaces that may contain any combination of events, fields, and methods. -/// -/// Field members are passed as three separate flat lists (one per tag type): -/// - `fields_notifier[name:type,...]` - `pub name: R::FieldSubscriber` (subscribe/notifications) -/// - `fields_getter[name:type,...]` - `pub name_get: R::FieldGetCaller` + `get_name()` wrapper -/// - `fields_setter[name:type,...]` - `pub name_set: R::FieldSetCaller` + `set_name(val)` wrapper -/// -/// A field with multiple tags (e.g. `WithGetter + WithSetter`) appears in multiple lists. -/// Method members get positional-arg wrapper functions via `_gen_method_wrapper!`. -#[doc(hidden)] -#[macro_export] -macro_rules! interface_consumer_mixed { - ( - $id:ident, - events[$($ev_name:ident : $ev_type:ty ,)*], - fields_notifier[$($fin_name:ident : $fin_type:ty ,)*], - fields_getter[$($fig_name:ident : $fig_type:ty ,)*], - fields_setter[$($fis_name:ident : $fis_type:ty ,)*], - methods[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] - ) => { - score_com::paste::paste! { - pub struct [<$id Consumer>] { - $( - pub $ev_name: R::Subscriber<$ev_type>, - )* - $( - pub $fin_name: R::FieldSubscriber<$fin_type>, - )* - $( - pub [<$fig_name _get>]: R::FieldGetCaller<$fig_type>, - )* - $( - pub [<$fis_name _set>]: R::FieldSetCaller<$fis_type>, - )* - $( - pub $me_name: R::MethodCaller<($($me_arg_ty,)*), $me_ret>, - )* - } - - impl score_com::Consumer for [<$id Consumer>] { - fn new(instance_info: R::ConsumerInfo) -> Self { - [<$id Consumer>] { - $( - $ev_name: R::Subscriber::new( - stringify!($ev_name), - instance_info.clone() - ).expect(&format!( - "Failed to create subscriber for {}", - stringify!($ev_name) - )), - )* - $( - $fin_name: R::FieldSubscriber::new( - stringify!($fin_name), - instance_info.clone() - ).expect(&format!( - "Failed to create field subscriber for {}", - stringify!($fin_name) - )), - )* - $( - [<$fig_name _get>]: - as score_com::MethodCaller<(), $fig_type, R>>::new( - concat!(stringify!($fig_name), "_get"), - instance_info.clone() - ).expect(&format!( - "Failed to create field get caller for {}", - stringify!($fig_name) - )), - )* - $( - [<$fis_name _set>]: - as score_com::MethodCaller<($fis_type,), $fis_type, R>>::new( - concat!(stringify!($fis_name), "_set"), - instance_info.clone() - ).expect(&format!( - "Failed to create field set caller for {}", - stringify!($fis_name) - )), - )* - $( - $me_name: - as score_com::MethodCaller<($($me_arg_ty,)*), $me_ret, R>>::new( - stringify!($me_name), - instance_info.clone() - ).expect(&format!( - "Failed to create method caller for {}", - stringify!($me_name) - )), - )* - } - } - } - - // Method wrappers: positional-arg convenience functions via MethodCallInput. - // Field get/set wrappers: async get/set via FieldGetCaller/FieldSetCaller. - // subscribe() is available directly on the struct field for WithNotifier fields. - impl [<$id Consumer>] { - $( - $crate::_gen_method_wrapper!($me_name ($($me_arg_ty),*) -> $me_ret); - )* - $( - /// Asynchronously get the current value of the field. - /// Returns a future that resolves to `Result>`. - /// Independent of subscription lifecycle. - pub fn []<'a>( - &'a self, - ) -> impl core::future::Future::MethodReturnSample<$fig_type>>> + 'a { - score_com::MethodCaller::invoke_with_copy(&self.[<$fig_name _get>], ()) - } - )* - $( - /// Asynchronously set the value of the field. - /// Returns a future that resolves to `Result>` - /// containing the confirmed field value from the producer. - /// Independent of subscription lifecycle. - pub fn []<'a>( - &'a self, - value: $fis_type, - ) -> impl core::future::Future::MethodReturnSample<$fis_type>>> + 'a { - score_com::MethodCaller::invoke_with_copy(&self.[<$fis_name _set>], (value,)) - } - )* - } - } - }; -} - /// Generates `{id}Producer`, `{id}OfferedProducer`, and all trait implementations for /// interfaces that may contain any combination of events, fields, and methods. /// @@ -1099,104 +733,6 @@ macro_rules! interface_producer_mixed { }; } -/// Entry-point wrapper generator. -/// Every generated wrapper returns `impl Future>> + '_`. -/// -/// # Generated call sites -/// ```text -/// consumer.method(val).await - copy path - val: ArgType -/// consumer.method(ptr).await - zero-copy - ptr: MethodInArgPtr -/// ``` -#[doc(hidden)] -#[macro_export] -macro_rules! _gen_method_wrapper { - // 0 args - invoke_with_copy directly; no zero-copy path (nothing to allocate). - // This is for kind of `get` methods that take no arguments and return a value. - ($me_name:ident () -> $me_ret:ty) => { - pub fn $me_name<'a>(&'a self) -> impl core::future::Future::MethodReturnSample<$me_ret>>> + 'a { - score_com::MethodCaller::invoke_with_copy(&self.$me_name, ()) - } - }; - // 1-N args - delegate to the self-counting recursive macro. - ($me_name:ident ($($t:ty),+) -> $me_ret:ty) => { - $crate::_gen_method_wrapper_collect!( - $me_name -> $me_ret ; - @counter[] - @acc[] - @types[$($t),+] - ); - }; -} - -/// Recursive macro for `_gen_method_wrapper!`. -/// -/// Self-counting: instead of zipping the method's positional type list against a -/// pre-defined pool of `(arg_name, generic_name)` identifiers, this recursive macro synthesizes -/// a fresh, unique `(argN : _AN : TypeN)` triplet at each recursion step directly from a -/// growing counter of `n` marker tokens (via `paste!`), then calls -/// `_gen_method_wrapper_body!` once the type list is exhausted. -/// -/// This mirrors the self-contained recursion used by `impl_all_arities!` in -/// `method_arities.rs`: there is no separate pool to keep in sync, and no fixed -/// argument-count limit - any arity supported by `method_arities.rs` works automatically. -#[doc(hidden)] -#[macro_export] -macro_rules! _gen_method_wrapper_collect { - // Base: all types consumed - emit the function via the body macro. - ( - $me_name:ident -> $me_ret:ty ; - @counter[$($n:tt)*] - @acc[$($acc:tt),*] - @types[] - ) => { - $crate::_gen_method_wrapper_body!($me_name -> $me_ret ; [$($acc),*]); - }; - - // Step: consume one type, grow the counter by one `n`, and synthesize a fresh - // (param, generic) identifier pair from the counter via `paste!`. - ( - $me_name:ident -> $me_ret:ty ; - @counter[$($n:tt)*] - @acc[$($acc:tt),*] - @types[$t:ty $(, $rest_t:ty)*] - ) => { - score_com::paste::paste! { - $crate::_gen_method_wrapper_collect!( - $me_name -> $me_ret ; - @counter[$($n)* n] - @acc[$($acc,)* ([] : [<_A $($n)*>] : $t)] - @types[$($rest_t),*] - ); - } - }; -} - -/// Generates the wrapper function from an accumulated list of `(argN : _AN : TypeN)`. -/// -/// This generates a wrapper function template. -/// All arities use this one arm - the function body is written once, not duplicated per arity. -/// Called by `_gen_method_wrapper_collect!` after it has built the full triplet list. -/// -/// The generated function returns `impl Future>> + 'a` so callers -/// can `.await` the method call, e.g. `consumer.method_name(arg0).await?`. -#[doc(hidden)] -#[macro_export] -macro_rules! _gen_method_wrapper_body { - ($me_name:ident -> $me_ret:ty ; [$(($p:ident : $g:ident : $c:ty)),+]) => { - pub fn $me_name<'a, $($g),+>( - &'a self, - $($p: $g),+ - ) -> impl core::future::Future::MethodReturnSample<$me_ret>>> + 'a - where - ($($g,)+): score_com::MethodCallInput<($($c,)+), $me_ret, R>, - R::MethodCaller<($($c,)+), $me_ret>: - score_com::MethodCaller<($($c,)+), $me_ret, R>, - { - score_com::MethodCallInput::invoke(($($p,)+), &self.$me_name) - } - }; -} - mod tests { /// ``` /// mod my_module { diff --git a/score/mw/com/rust/score_com_concept/lib.rs b/score/mw/com/rust/score_com_concept/lib.rs index 4976a42f7..2963b9b54 100644 --- a/score/mw/com/rust/score_com_concept/lib.rs +++ b/score/mw/com/rust/score_com_concept/lib.rs @@ -23,14 +23,15 @@ mod concept; mod error; mod field_concept; -mod interface_macros; +mod interface_consumer_macros; +mod interface_producer_macros; mod method_arities_macros; mod method_concept; mod reloc; pub use concept::*; pub use error::*; pub use field_concept::*; -pub use interface_macros::{HandlerNotSet, HandlerSet, Init, Uninit, WithGetter, WithNotifier, WithSetter}; +pub use interface_consumer_macros::{HandlerNotSet, HandlerSet, Init, Uninit, WithGetter, WithNotifier, WithSetter}; pub use method_concept::*; #[doc(hidden)] pub use paste; From bb7545a5dec6fede01b49f8dda6f70bf8c3c4055 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Thu, 30 Jul 2026 16:25:30 +0530 Subject: [PATCH 20/25] Rust::com Update field design for registration APIs * Updated set and get APIs for registration --- .../com-api-example/src/field_producer.rs | 25 ++++++------ .../com-api-runtime-lola/field_producer.rs | 20 ++++++---- .../com-api/com-api-runtime-mock/runtime.rs | 8 +++- .../rust/score_com_concept/field_concept.rs | 38 +++++++++++++++---- .../score_com_macros/type_state_validator.rs | 14 +++---- 5 files changed, 67 insertions(+), 38 deletions(-) diff --git a/score/mw/com/example/com-api-example/src/field_producer.rs b/score/mw/com/example/com-api-example/src/field_producer.rs index fa360bd80..a1922f5e4 100644 --- a/score/mw/com/example/com-api-example/src/field_producer.rs +++ b/score/mw/com/example/com-api-example/src/field_producer.rs @@ -23,10 +23,10 @@ type VehicleFieldProducer = ::Producer type VehicleFieldOfferedProducer = <::Producer as Producer>::OfferedProducer; -// Below function just demonstrate the field APIs usage -// This build fine but it can not run because we have not implemented the field APIs in Lola runtime yet. +// Below function demonstrates the field APIs usage. +// This builds fine, but it cannot run as the field APIs in Lola runtime are not implemented yet. -// Producer creation and intialization of fields with initial values and set handlers for the fields +// Producer creation and initialization of fields with initial values and set handlers for the fields. // It will return the offered producer instance which can be used to update the fields. fn create_producer_field( runtime: &R, @@ -47,21 +47,18 @@ where // Must register handlers and initialize all fields before offer() is available let offered = producer .init() - .register_set_handler_left_tire(move |val: &Tire| { + .register_set_handler_left_tire(move |val: Tire| { println!("Received tire pressure update: {:?}", val); - // Additional logic to handle the tire pressure update can be added here - // For example, we can increment value or conver unit and update the field again. + // Additional logic: inspect or act on the accepted value (logging, telemetry, etc.). // TODO: in working example add that logic to demonstrate the set handler usage. - // Note: I think producer may be need clone ? }) - .expect("Failed to register set handler for left_tire") - .register_set_handler_exhaust(|_val: &Exhaust| { + .register_set_handler_exhaust(|val: Exhaust| { + let _ = val; println!("Received exhaust update"); }) - .expect("Failed to register set handler for exhaust") - .update_left_tire(&initial_tire_value) + .update_left_tire(initial_tire_value) .expect("Failed to update left_tire field") - .update_exhaust(&initial_exhaust_value) + .update_exhaust(initial_exhaust_value) .expect("Failed to update exhaust field") .offer() .expect("Failed to offer producer instance"); @@ -76,10 +73,10 @@ fn offered_producer_process(offered_producer: VehicleFieldOfferedPro let new_exhaust_value = Exhaust {}; offered_producer .left_tire - .update(&new_tire_value) + .update(new_tire_value) .expect("Failed to update left_tire field"); offered_producer .exhaust - .update(&new_exhaust_value) + .update(new_exhaust_value) .expect("Failed to update exhaust field"); } diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs index 06a867912..5ca3f2f75 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs @@ -92,16 +92,22 @@ impl FieldPublisher> fn allocate(&self) -> Result> { todo!() } - fn update(&self, _value: &T) -> Result<()> { + fn update(&self, _value: T) -> Result<()> { + todo!() + } + fn register_set_handler(&self, _callback: impl Fn(T) + Send + 'static) { + // When the middleware receives a set request from a consumer: + // - Invoke the callback with a mutable reference to the proposed value so it can + // validate or modify it in-place (matching C++ `void(FieldType&)` semantics). + // - Use the (possibly modified) value as the final field value to store and send. + // Execution model (thread pool vs. async task pool) to be decided at implementation time. todo!() } - fn register_set_handler(&self, _callback: impl Fn(&T) + Send + 'static) -> Result<()> { - //If waker get the notification form FFI call then - //Create a task to call the callback with value. - //Thread pool is a option here to run the callback in a separate thread. - //But i feel we still need to think about exection order of that callback, - //Because separate thread can raise concurrency issue / race condition. + fn register_get_handler(&self, _callback: impl Fn() -> T + Send + 'static) { + // When the middleware receives a get request from a consumer: + // - Invoke the callback and return its result to the consumer. + // Default behaviour (if not registered): return the last Update()d value. todo!() } } diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs index 1b06711a3..4255c51c2 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-mock/runtime.rs @@ -809,11 +809,15 @@ impl FieldPublisher for MockFieldPublis }) } - fn update(&self, _value: &T) -> Result<()> { + fn update(&self, _value: T) -> Result<()> { todo!() } - fn register_set_handler(&self, _callback: impl Fn(&T) + Send + 'static) -> Result<()> { + fn register_set_handler(&self, _callback: impl Fn(T) + Send + 'static) { + todo!() + } + + fn register_get_handler(&self, _callback: impl Fn() -> T + Send + 'static) { todo!() } } diff --git a/score/mw/com/rust/score_com_concept/field_concept.rs b/score/mw/com/rust/score_com_concept/field_concept.rs index f714a94fd..5d4e6dc80 100644 --- a/score/mw/com/rust/score_com_concept/field_concept.rs +++ b/score/mw/com/rust/score_com_concept/field_concept.rs @@ -55,6 +55,11 @@ pub trait FieldSubscriber: /// In addition to the base subscription APIs, a field subscription exposes: /// - `get_num_new_samples_available()` — how many fresh samples are ready to receive. /// - `get_free_sample_count()` — remaining capacity in the subscription buffer. +/// +/// Note: In C++ both `ProxyEvent` and `ProxyField` expose `GetNumNewSamplesAvailable()` and +/// `GetFreeSampleCount()` (field delegates to the underlying event base). These therefore belong +/// in the base `concept::Subscription` trait in Rust as well; they are placed here temporarily +/// until a follow-up PR moves them up to `Subscription`. pub trait FieldSubscription: concept::Subscription { @@ -90,18 +95,35 @@ pub trait FieldPublisher { /// /// # Returns /// Return the result of `Result<()>` which contains the status of the update operation. - fn update(&self, value: &T) -> Result<()>; + /// Update the value of the field with the provided value. + /// The value is taken by value; the FFI layer handles the necessary copy into the shared + /// memory slot internally — the same pattern as `Publisher::send(value: T)` for events. + /// For zero-copy writes use `allocate()` instead. + fn update(&self, value: T) -> Result<()>; - /// Register a callback function to handle the set operation for the field. - /// It will create new task or thread to handle the set operation callback function, - /// which will be mostly done using thread pool or async task pool, will be decided at the time of implementation. + /// Register a callback invoked by the middleware whenever a consumer calls the field setter. + /// The callback receives the proposed new value **by value** as a notification — the FFI + /// layer has already committed the value to storage (same as `Publisher::send()` for events). + /// The callback is for side effects only (e.g. logging, triggering downstream logic). + /// + /// This registration is infallible (like `MethodHandler::register_handler`). /// /// # Parameters - /// * `callback` - The callback function to handle the set operation for the field. + /// * `callback` - Receives the accepted value; return type is `()`. + fn register_set_handler(&self, callback: impl Fn(T) + Send + 'static); + + /// Register a callback invoked by the middleware whenever a consumer calls the field getter. + /// The callback returns the value that will be delivered back to the consumer. /// - /// # Returns - /// Return the result of `Result<()>` which contains the status of the register operation. - fn register_set_handler(&self, callback: impl Fn(&T) + Send + 'static) -> Result<()>; + /// Note: In the C++ implementation the default get handler is registered automatically by the + /// framework (it returns the last `Update`d value). Rust exposes this explicitly so producers + /// can install custom read logic if needed. + /// + /// This registration is infallible (like `MethodHandler::register_handler`). + /// + /// # Parameters + /// * `callback` - The callback function; must return the current field value. + fn register_get_handler(&self, callback: impl Fn() -> T + Send + 'static); } /// FieldSampleMut trait is used to update the value of the field sample for zero-copy API. diff --git a/score/mw/com/rust/score_com_macros/type_state_validator.rs b/score/mw/com/rust/score_com_macros/type_state_validator.rs index d70bf22f0..e6d0dd223 100644 --- a/score/mw/com/rust/score_com_macros/type_state_validator.rs +++ b/score/mw/com/rust/score_com_macros/type_state_validator.rs @@ -221,7 +221,7 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { { pub fn #update_fn( mut self, - value: &#inner_ty, + value: #inner_ty, ) -> score_com::Result<#validator_name<#runtime_param_name, #(#after),*>> { self.producer.#field_ident.update(value)?; Ok(#validator_name { @@ -268,17 +268,17 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { <#runtime_param_name as score_com::Runtime>::FieldPublisher<#inner_ty>: Send, { pub fn #register_fn( - mut self, + self, handler: F, - ) -> score_com::Result<#validator_name<#runtime_param_name, #(#after),*>> + ) -> #validator_name<#runtime_param_name, #(#after),*> where - F: Fn(&#inner_ty) + Send + 'static, + F: Fn(#inner_ty) + Send + 'static, { - self.producer.#field_ident.register_set_handler(handler)?; - Ok(#validator_name { + self.producer.#field_ident.register_set_handler(handler); + #validator_name { producer: self.producer, _phantom: core::marker::PhantomData, - }) + } } } } From e89ee9f693dcb471654483fbcf4bd25730181427 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Thu, 30 Jul 2026 23:22:31 +0530 Subject: [PATCH 21/25] Rust::com Update the sample example app --- .../example/com-api-example/src/field_consumer.rs | 11 ++++++++++- .../example/com-api-example/src/field_producer.rs | 12 +++++++++++- score/mw/com/example/com-api-example/src/lib.rs | 3 ++- .../example/com-api-example/src/method_consumer.rs | 7 +++++-- .../example/com-api-example/src/method_producer.rs | 5 +++-- score/mw/com/rust/score_com_concept/field_concept.rs | 7 +------ 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/score/mw/com/example/com-api-example/src/field_consumer.rs b/score/mw/com/example/com-api-example/src/field_consumer.rs index 9cae42b6f..dc7047d7a 100644 --- a/score/mw/com/example/com-api-example/src/field_consumer.rs +++ b/score/mw/com/example/com-api-example/src/field_consumer.rs @@ -11,7 +11,13 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#![allow(unused)] +// This file demonstrate the usage of consumer field APIs, which are generated for the VehicleFieldInterface. + +// Notes: we are creating consumer instance specific for field here but this is just for demonstration purpose, +// for same consumer instance method / event/ field can be subscribed as per subscribe interface. + +// All the functions and types in this file are just for demonstration purpose, +// as this are not part of any callable because of that unused warning is suppressed for this file. use score_com::{ Builder, FindServiceSpecifier, InstanceSpecifier, @@ -20,9 +26,11 @@ use score_com::{ use com_api_gen::{Tire, VehicleFieldInterface}; +#[allow(dead_code)] type VehicleFieldConsumer = ::Consumer; // create the consumer. +#[allow(dead_code)] fn create_consumer_field( runtime: &R, service_id: InstanceSpecifier, @@ -54,6 +62,7 @@ fn create_consumer_field( // // Because subscribe() takes `left_tire` by value, extract the callers before subscribing // if both are needed in the same async context. +#[allow(dead_code)] async fn consumer_processing_field(consumer: VehicleFieldConsumer) { // Async get via the generated get_left_tire() wrapper. // Uses MethodCaller<(), Tire> under the hood - reuses Method infrastructure. diff --git a/score/mw/com/example/com-api-example/src/field_producer.rs b/score/mw/com/example/com-api-example/src/field_producer.rs index a1922f5e4..e47a2e560 100644 --- a/score/mw/com/example/com-api-example/src/field_producer.rs +++ b/score/mw/com/example/com-api-example/src/field_producer.rs @@ -11,15 +11,23 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#![allow(unused)] +// This file demonstrate the usage of producer field APIs, which are generated for the VehicleFieldInterface. + +// Notes: we are creating producer instance specific for field here but this is just for demonstration purpose, +// for same producer instance method / event/ field can be offered as per offer interface. + +// All the functions and types in this file are just for demonstration purpose, +// as this are not part of any callable because of that unused warning is suppressed for this file. use score_com::{Builder, FieldPublisher, InstanceSpecifier, Interface, Producer, Runtime}; use com_api_gen::{Exhaust, Tire, VehicleFieldInterface}; // VehicleFieldProducer is the producer type for the VehicleField interface (before offering) +#[allow(dead_code)] type VehicleFieldProducer = ::Producer; // VehicleFieldOfferedProducer is the offered producer type for the VehicleField interface (fields support update/set-handler) +#[allow(dead_code)] type VehicleFieldOfferedProducer = <::Producer as Producer>::OfferedProducer; @@ -28,6 +36,7 @@ type VehicleFieldOfferedProducer = // Producer creation and initialization of fields with initial values and set handlers for the fields. // It will return the offered producer instance which can be used to update the fields. +#[allow(dead_code)] fn create_producer_field( runtime: &R, service_id: InstanceSpecifier, @@ -67,6 +76,7 @@ where } // Function to demonstrate the usage of the offered producer to update fields +#[allow(dead_code)] fn offered_producer_process(offered_producer: VehicleFieldOfferedProducer) { // Use the offered producer to update fields let new_tire_value = Tire { pressure: 32.0 }; diff --git a/score/mw/com/example/com-api-example/src/lib.rs b/score/mw/com/example/com-api-example/src/lib.rs index aeec3dbe5..080e2960c 100644 --- a/score/mw/com/example/com-api-example/src/lib.rs +++ b/score/mw/com/example/com-api-example/src/lib.rs @@ -12,11 +12,12 @@ ********************************************************************************/ pub mod consumer; -// Method modules are just for demonstration purpose, as runtime implementation is not available for method APIs. +// Method/Field/Mixed modules are just for demonstration purpose, as runtime implementation is not available yet. mod field_consumer; mod field_producer; mod method_consumer; mod method_producer; +mod mixed_monitor; pub mod producer; pub use consumer::VehicleMonitorConsumer; pub use producer::VehicleMonitorProducer; diff --git a/score/mw/com/example/com-api-example/src/method_consumer.rs b/score/mw/com/example/com-api-example/src/method_consumer.rs index 81c7c5cdc..7fb0b9390 100644 --- a/score/mw/com/example/com-api-example/src/method_consumer.rs +++ b/score/mw/com/example/com-api-example/src/method_consumer.rs @@ -21,8 +21,6 @@ // All the functions and types in this file are just for demonstration purpose, // as this are not part of any callable because of that unused warning is suppressed for this file. -#![allow(unused)] - use score_com::{ Builder, FindServiceSpecifier, InstanceSpecifier, Interface, MethodCaller, MethodInArgMaybeUninit, Runtime, ServiceDiscovery, @@ -30,10 +28,12 @@ use score_com::{ use com_api_gen::{Tire, VehicleMethodsInterface}; +#[allow(dead_code)] type VehicleMethodConsumer = ::Consumer; // These functions are just to demonstrate the method APIs, and they can not be used in main of example app, // as runtime implementation is not available for method APIs. +#[allow(dead_code)] fn create_consumer_method( runtime: &R, service_id: InstanceSpecifier, @@ -62,6 +62,7 @@ fn create_consumer_method( // Copy path: single positional argument. // Demonstrates calling a method with a single argument, where the argument is copied into the method call. // Zero-copy path: allocate, write, then call the method with allocaed args. +#[allow(dead_code)] async fn consumer_method_processing(consumer: VehicleMethodConsumer) { // Copy path: single positional argument — no tuple needed. let tire = Tire { pressure: 30.0 }; @@ -88,6 +89,7 @@ async fn consumer_method_processing(consumer: VehicleMethodConsumer< } // Get Method call which has no argument and return a value, which is also async. +#[allow(dead_code)] async fn method_get_call(consumer: VehicleMethodConsumer) { // Copy path: zero-argument method — empty parens, no empty-tuple needed. // it returns a `Result>` @@ -101,6 +103,7 @@ async fn method_get_call(consumer: VehicleMethodConsumer) { // two arguments method. // It demonstrates calling a method with two arguments, where the arguments are copied into the method call. // It also demonstrates the zero-copy path, where the arguments are allocated, written, and then passed to the method call. +#[allow(dead_code)] async fn consumer_processing(consumer: VehicleMethodConsumer) { // Copy path: two arguments method. let tire1 = Tire { pressure: 31.0 }; diff --git a/score/mw/com/example/com-api-example/src/method_producer.rs b/score/mw/com/example/com-api-example/src/method_producer.rs index 4c612d313..92c0d3c45 100644 --- a/score/mw/com/example/com-api-example/src/method_producer.rs +++ b/score/mw/com/example/com-api-example/src/method_producer.rs @@ -18,12 +18,11 @@ // All the functions and types in this file are just for demonstration purpose, // as this are not part of any callable because of that unused warning is suppressed for this file. -#![allow(unused)] - use score_com::{Builder, InstanceSpecifier, Interface, Producer, Runtime}; use com_api_gen::{Tire, VehicleMethodsInterface}; +#[allow(dead_code)] type VehicleMethodOfferedProducer = <::Producer as Producer>::OfferedProducer; @@ -39,6 +38,7 @@ type VehicleMethodOfferedProducer = // as the offer method will require all method handlers to be registered before offering the producer instance. // Here assumption of use is user must call`init()` and register all method handlers , // direct call to `offer()` API will panic. +#[allow(dead_code)] fn create_producer_method( runtime: &R, service_id: InstanceSpecifier, @@ -67,6 +67,7 @@ fn create_producer_method( .expect("Failed to offer producer instance") } +#[allow(dead_code)] fn process_left_tire(tire: Tire) { // do some processing with the tire data println!("Processing left tire pressure: {:?}", tire); diff --git a/score/mw/com/rust/score_com_concept/field_concept.rs b/score/mw/com/rust/score_com_concept/field_concept.rs index 5d4e6dc80..52fd7c7f4 100644 --- a/score/mw/com/rust/score_com_concept/field_concept.rs +++ b/score/mw/com/rust/score_com_concept/field_concept.rs @@ -32,7 +32,7 @@ // with `get_{name}()` / `set_{name}()` async convenience wrappers. // On the producer side, FieldPublisher keeps update() + register_set_handler() unchanged. -use crate::*; +use crate::concept::{self, CommData, Result, Runtime, SampleMaybeUninit}; use std::fmt::Debug; /// `FieldSubscriber` is used to subscribe to a field and receive update notifications. @@ -55,11 +55,6 @@ pub trait FieldSubscriber: /// In addition to the base subscription APIs, a field subscription exposes: /// - `get_num_new_samples_available()` — how many fresh samples are ready to receive. /// - `get_free_sample_count()` — remaining capacity in the subscription buffer. -/// -/// Note: In C++ both `ProxyEvent` and `ProxyField` expose `GetNumNewSamplesAvailable()` and -/// `GetFreeSampleCount()` (field delegates to the underlying event base). These therefore belong -/// in the base `concept::Subscription` trait in Rust as well; they are placed here temporarily -/// until a follow-up PR moves them up to `Subscription`. pub trait FieldSubscription: concept::Subscription { From f02fa96ddb924f08c53ad543a4a00bba0b55cbba Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Fri, 31 Jul 2026 09:22:45 +0530 Subject: [PATCH 22/25] Rust::com Doc test and additinal example added for Method and Field --- .../com-api-example/src/method_producer.rs | 1 - .../com-api-example/src/mixed_monitor.rs | 316 ++++++++++++++++++ score/mw/com/rust/score_com_concept/BUILD | 8 +- .../interface_consumer_macros.rs | 163 --------- .../interface_producer_macros.rs | 255 +++++++++++++- 5 files changed, 565 insertions(+), 178 deletions(-) create mode 100644 score/mw/com/example/com-api-example/src/mixed_monitor.rs diff --git a/score/mw/com/example/com-api-example/src/method_producer.rs b/score/mw/com/example/com-api-example/src/method_producer.rs index 92c0d3c45..78caf31aa 100644 --- a/score/mw/com/example/com-api-example/src/method_producer.rs +++ b/score/mw/com/example/com-api-example/src/method_producer.rs @@ -61,7 +61,6 @@ fn create_producer_method( "Received update_front_tires_pressure call with tire1: {:?}, tire2: {:?}", tire1, tire2 ); - () }) .offer() .expect("Failed to offer producer instance") diff --git a/score/mw/com/example/com-api-example/src/mixed_monitor.rs b/score/mw/com/example/com-api-example/src/mixed_monitor.rs new file mode 100644 index 000000000..fb1c319e7 --- /dev/null +++ b/score/mw/com/example/com-api-example/src/mixed_monitor.rs @@ -0,0 +1,316 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// This file demonstrates the usage of the mixed VehicleMonitorInterface, which combines +// Events, Fields, and Methods in a single interface definition. +// +// VehicleMonitorInterface is defined as: +// interface VehicleMonitor { +// Id = "VehicleMonitorInterface", +// left_tire: Event, // event +// exhaust: Event, // event +// left_tire_field: Field, // field +// exhaust_field: Field, // field +// update_tire_pressure(Tire) -> (), // method +// update_front_tires_pressure(Tire, Tire) -> (), // method +// get_tire_pressure() -> Tire, // method +// } +// +// Producer side (skeleton): +// - Events are published via `offered.left_tire.send(...)` / `offered.exhaust.send(...)`. +// - Fields require an initial value (`update_left_tire_field` / `update_exhaust_field`) and +// a set-handler (`register_set_handler_*_field`) before `offer()` is available +// (both enforced at compile time via type state). +// - Methods require all handlers to be registered before `offer()` is available (same type state). +// +// Consumer side (proxy): +// - Events are subscribed via `left_tire.subscribe()` / `exhaust.subscribe()`. +// - Field notifications are subscribed via `left_tire_field.subscribe()`. +// - Field get/set are async wrappers backed by MethodCaller: +// `consumer.get_left_tire_field().await` / `consumer.set_left_tire_field(val).await` +// - Methods are called as async wrappers: `consumer.update_tire_pressure(tire).await` +// +// This builds fine, but cannot run as field and method APIs in Lola runtime are not implemented yet. + +use score_com::{ + Builder, FieldPublisher, FindServiceSpecifier, InstanceSpecifier, Interface, MethodCaller, + MethodInArgMaybeUninit, Producer, Publisher, Runtime, SampleContainer, ServiceDiscovery, + Subscriber, Subscription, +}; + +use com_api_gen::{Exhaust, Tire, VehicleMonitorInterface}; + +// Type aliases + +#[allow(dead_code)] +type VehicleMonitorProducer = ::Producer; + +#[allow(dead_code)] +type VehicleMonitorOfferedProducer = + <::Producer as Producer>::OfferedProducer; + +#[allow(dead_code)] +type VehicleMonitorConsumer = ::Consumer; + +// Producer + +/// Create and offer a VehicleMonitor producer. +/// +/// The type-state chain on `init()` enforces at **compile time** that: +/// - every Field has an initial value set (`update_*_field`) +/// - every Field has a set-handler registered (`register_set_handler_*_field`) +/// - every Method has a handler registered (`register_*_handler`) +/// +/// Calling `offer()` before satisfying all of the above is a **compile error**. +#[allow(dead_code)] +fn create_monitor_producer( + runtime: &R, + service_id: InstanceSpecifier, + initial_tire: Tire, + initial_exhaust: Exhaust, +) -> VehicleMonitorOfferedProducer +where + ::FieldPublisher: Send + Sync, + ::FieldPublisher: Send, +{ + let producer = runtime + .producer_builder::(service_id) + .build() + .expect("Failed to build VehicleMonitor producer"); + + producer + .init() + // Field: left_tire_field + // Register set-handler: called by the middleware when a consumer calls Set on this field. + // Receives the accepted value by value for inspection / side effects. + .register_set_handler_left_tire_field(|val: Tire| { + println!("[Producer] set_handler left_tire_field: {:?}", val); + // Additional validation or side-effect logic can go here. + }) + // Set initial field value (required before offer()). + .update_left_tire_field(initial_tire) + .expect("Failed to set initial value for left_tire_field") + // Field: exhaust_field + .register_set_handler_exhaust_field(|val: Exhaust| { + let _ = val; + println!("[Producer] set_handler exhaust_field"); + }) + .update_exhaust_field(initial_exhaust) + .expect("Failed to set initial value for exhaust_field") + // Method: update_tire_pressure(Tire) -> () + .register_update_tire_pressure_handler(|tire: Tire| { + println!("[Producer] update_tire_pressure called: {:?}", tire); + }) + // Method: update_front_tires_pressure(Tire, Tire) -> () + .register_update_front_tires_pressure_handler(|tire1: Tire, tire2: Tire| { + println!( + "[Producer] update_front_tires_pressure called: {:?}, {:?}", + tire1, tire2 + ); + }) + // Method: get_tire_pressure() -> Tire + .register_get_tire_pressure_handler(|| { + println!("[Producer] get_tire_pressure called"); + // Return the current field value; in a real implementation this would + // read from the last Update()d field value. + Tire { pressure: 32.0 } + }) + // All states satisfied → offer() is available. + .offer() + .expect("Failed to offer VehicleMonitor producer") +} + +/// Demonstrate publishing events on the already-offered producer. +/// Events do not require type-state initialization; they can be published at any time. +#[allow(dead_code)] +fn publish_events(offered: &VehicleMonitorOfferedProducer) { + // Publish an event update for left_tire (event member, not field). + offered + .left_tire + .send(Tire { pressure: 33.5 }) + .expect("Failed to publish left_tire event"); + + // Publish an event update for exhaust. + offered + .exhaust + .send(Exhaust {}) + .expect("Failed to publish exhaust event"); +} + +/// Demonstrate updating field values on the already-offered producer. +#[allow(dead_code)] +fn update_fields(offered: &VehicleMonitorOfferedProducer) { + // Update field value sends the new value to all field subscribers. + offered + .left_tire_field + .update(Tire { pressure: 34.0 }) + .expect("Failed to update left_tire_field"); + + offered + .exhaust_field + .update(Exhaust {}) + .expect("Failed to update exhaust_field"); +} + +// Consumer + +/// Create a VehicleMonitor consumer by discovering the service instance. +#[allow(dead_code)] +fn create_monitor_consumer( + runtime: &R, + service_id: InstanceSpecifier, +) -> VehicleMonitorConsumer { + let discovery = runtime + .find_service::(FindServiceSpecifier::Specific(service_id)); + + let instances = discovery + .get_available_instances() + .expect("Failed to get available service instances"); + + instances + .into_iter() + .next() + .expect("No VehicleMonitor service instance found") + .build() + .expect("Failed to build VehicleMonitor consumer") +} + +/// Demonstrate all consumer-side APIs on the VehicleMonitorInterface: +/// - Subscribe to events and poll for samples. +/// - Async field get/set via generated MethodCaller wrappers. +/// - Subscribe to field notifications and poll for updates. +/// - Async method calls (copy and zero-copy paths). +#[allow(dead_code)] +async fn consume_monitor(consumer: VehicleMonitorConsumer) { + // TODO: Ordering workaround subscribe(self) vs. whole-struct &self methods + // + // The current Subscriber trait signature is: + // fn subscribe(self, max_num_samples: usize) -> Result + // + // This takes the subscriber by value, which partially moves the corresponding + // field (e.g. consumer.left_tire) out of the consumer struct. Once any field + // is partially moved, Rust's borrow checker rejects whole-struct &self borrows + // such as consumer.get_left_tire_field() or consumer.update_tire_pressure(). + // + // Workaround Just for example: reorder so all whole-struct &self calls (field get/set, methods) + // happen FIRST, and all subscribe() calls happen LAST at that point individual + // field access (consumer.left_tire_field) still works because Rust tracks partial + // moves per field, not per struct. + // + // TODO Suggest fix: change subscribe to take &mut self: + // fn subscribe(&mut self, max_num_samples: usize) -> Result + // With &mut self, no field is ever moved out, so consumer remains fully usable in any order. + // With this change, unsubscribe return also need to change. + + // Fields (async get/set) + // Async get uses MethodCaller<(), Tire> under the hood. + match consumer.get_left_tire_field().await { + Ok(result) => println!("[Consumer] left_tire_field get: {:?}", *result), + Err(e) => eprintln!("[Consumer] left_tire_field get failed: {:?}", e), + } + + // Async set uses MethodCaller<(Tire,), Tire> under the hood. + // Returns the accepted value (after the producer's set-handler may have modified it). + match consumer.set_left_tire_field(Tire { pressure: 36.0 }).await { + Ok(result) => println!("[Consumer] left_tire_field set confirmed: {:?}", *result), + Err(e) => eprintln!("[Consumer] left_tire_field set failed: {:?}", e), + } + + // Methods + // Copy path: single argument. + match consumer.update_tire_pressure(Tire { pressure: 30.0 }).await { + Ok(_) => println!("[Consumer] update_tire_pressure OK"), + Err(e) => eprintln!("[Consumer] update_tire_pressure failed: {:?}", e), + } + + // Copy path: two arguments. + match consumer + .update_front_tires_pressure(Tire { pressure: 31.0 }, Tire { pressure: 32.0 }) + .await + { + Ok(_) => println!("[Consumer] update_front_tires_pressure OK"), + Err(e) => eprintln!("[Consumer] update_front_tires_pressure failed: {:?}", e), + } + + // Zero-copy path: allocate, write, then call. + let (uninit,) = consumer + .update_tire_pressure + .allocate() + .expect("Failed to allocate method argument"); + let tire_ptr = uninit.write(Tire { pressure: 35.0 }); + match consumer.update_tire_pressure(tire_ptr).await { + Ok(_) => println!("[Consumer] update_tire_pressure (zero-copy) OK"), + Err(e) => eprintln!("[Consumer] update_tire_pressure (zero-copy) failed: {:?}", e), + } + + // Zero-argument method returning a value. + match consumer.get_tire_pressure().await { + Ok(tire) => println!("[Consumer] get_tire_pressure: {:?}", *tire), + Err(e) => eprintln!("[Consumer] get_tire_pressure failed: {:?}", e), + } + + // Events + // subscribe(self) moves consumer.left_tire out of consumer (partial move). + // Whole-struct &self methods are not allowed after this point, but direct + // field access to other fields (e.g. consumer.left_tire_field below) still works. + { + let event_subscription = consumer + .left_tire + .subscribe(4) + .expect("Failed to subscribe to left_tire event"); + + let mut event_buf = SampleContainer::new(4); + match event_subscription.try_receive(&mut event_buf, 4) { + Ok(n) if n > 0 => { + while let Some(sample) = event_buf.pop_front() { + println!("[Consumer] left_tire event: {:?}", *sample); + } + } + _ => println!("[Consumer] No new left_tire event samples"), + } + // Drop event_buf before event_subscription: Sample<'_> borrows from the + // subscription, so the buffer must be gone before the subscription is dropped. + drop(event_buf); + // event_subscription dropped here (unsubscribed). + } + + // Fields (subscribe for notifications) + // consumer.left_tire is partially moved above, but consumer.left_tire_field is a + // distinct field and is still valid Rust tracks field moves individually. + { + let field_subscription = consumer + .left_tire_field + .subscribe(3) + .expect("Failed to subscribe to left_tire_field notifications"); + + let mut field_buf = SampleContainer::new(3); + match field_subscription.try_receive(&mut field_buf, 3) { + Ok(n) if n > 0 => { + while let Some(sample) = field_buf.pop_front() { + println!("[Consumer] left_tire_field notification: {:?}", *sample); + } + } + _ => println!("[Consumer] No new left_tire_field notifications"), + } + drop(field_buf); + // field_subscription dropped here (unsubscribed). + } + + // TODO: Uncomment when Runtime implementation is ready and + // subscribe() is changed to take &mut self (no partial move). + // match consumer.update_tire_pressure(Tire { pressure: 30.0 }).await { + // Ok(_) => println!("[Consumer] update_tire_pressure OK"), + // Err(e) => eprintln!("[Consumer] update_tire_pressure failed: {:?}", e), + // } + +} diff --git a/score/mw/com/rust/score_com_concept/BUILD b/score/mw/com/rust/score_com_concept/BUILD index 87707bac7..18ca24d93 100644 --- a/score/mw/com/rust/score_com_concept/BUILD +++ b/score/mw/com/rust/score_com_concept/BUILD @@ -38,7 +38,10 @@ rust_test( crate = ":score_com_concept", edition = "2024", tags = ["manual"], - deps = [":score_com_concept"], + deps = [ + ":score_com_concept", + "//score/mw/com/rust:score_com", + ], ) rust_doc_test( @@ -57,8 +60,5 @@ rust_unit_test( name = "score_com_concept-macros-unit-tests", srcs = ["interface_producer_macros.rs"], features = ["link_std_cpp_lib"], - # TODO: remove tags = ["manual"] once field or method PR is merged, - # Unit test failed because macro has field and method both types - tags = ["manual"], deps = ["//score/mw/com/rust:score_com"], ) diff --git a/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs b/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs index a7eb8872b..a1cc4846c 100644 --- a/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs @@ -86,169 +86,6 @@ macro_rules! interface_consumer { }; } -/// Per-tag struct field declaration helper for `interface_consumer_mixed!`. -/// -/// Called once per field member; iterates over the tag list and emits one struct field per tag. -/// - `WithNotifier` - `pub $name: R::FieldSubscriber<$type>,` -/// - `WithGetter` - `pub {name}_get: R::FieldGetCaller<$type>,` -/// - `WithSetter` - `pub {name}_set: R::FieldSetCaller<$type>,` -/// Any ordering of tags is supported; unrecognized tags produce a compile error. -/// -/// NOTE: Must be called inside a `score_com::paste::paste! { pub struct ... { HERE } }` block -/// since it uses `[<>]` identifier concatenation. -#[doc(hidden)] -#[macro_export] -macro_rules! _field_consumer_decl { - // Base: no more tags - ($fi_name:ident, $fi_type:ty, []) => {}; - - // WithNotifier: emit FieldSubscriber field, recurse - ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*]) => { - pub $fi_name: R::FieldSubscriber<$fi_type>, - $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); - }; - - // WithGetter: emit FieldGetCaller field - ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*]) => { - pub [<$fi_name _get>]: R::FieldGetCaller<$fi_type>, - $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); - }; - - // WithSetter: emit FieldSetCaller field - ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*]) => { - pub [<$fi_name _set>]: R::FieldSetCaller<$fi_type>, - $crate::_field_consumer_decl!($fi_name, $fi_type, [$($rest),*]); - }; - - // Unrecognized tag - ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*]) => { - compile_error!(concat!( - "interface!: unrecognized field tag `", - stringify!($unknown), - "`. Supported tags: WithGetter, WithSetter, WithNotifier." - )); - }; -} - -/// Per-tag struct field initializer helper for `interface_consumer_mixed!`. -/// -/// Emits one initializer expression per tag (used inside the `Consumer::new()` struct literal). -/// - `WithNotifier` - `$name: R::FieldSubscriber::new(...)` -/// - `WithGetter` - `{name}_get: as MethodCaller<(), T, R>>::new(...)` -/// - `WithSetter` - `{name}_set: as MethodCaller<(T,), T, R>>::new(...)` -/// -/// NOTE: Must be called inside a `score_com::paste::paste! { Struct { HERE } }` block. -#[doc(hidden)] -#[macro_export] -macro_rules! _field_consumer_init { - // Base: no more tags - ($fi_name:ident, $fi_type:ty, [], $instance_info:ident) => {}; - - // WithNotifier: emit FieldSubscriber init, recurse - ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*], $instance_info:ident) => { - $fi_name: R::FieldSubscriber::new( - stringify!($fi_name), - $instance_info.clone() - ).expect(&format!( - "Failed to create field subscriber for {}", - stringify!($fi_name) - )), - $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); - }; - - // WithGetter: emit FieldGetCaller init - ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*], $instance_info:ident) => { - [<$fi_name _get>]: - as score_com::MethodCaller<(), $fi_type, R>>::new( - concat!(stringify!($fi_name), "_get"), - $instance_info.clone() - ).expect(&format!( - "Failed to create field get caller for {}", - stringify!($fi_name) - )), - $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); - }; - - // WithSetter: emit FieldSetCaller init - ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*], $instance_info:ident) => { - [<$fi_name _set>]: - as score_com::MethodCaller<($fi_type,), $fi_type, R>>::new( - concat!(stringify!($fi_name), "_set"), - $instance_info.clone() - ).expect(&format!( - "Failed to create field set caller for {}", - stringify!($fi_name) - )), - $crate::_field_consumer_init!($fi_name, $fi_type, [$($rest),*], $instance_info); - }; - - // Unrecognized tag (already caught by _field_consumer_decl, but guard here too) - ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*], $instance_info:ident) => { - compile_error!(concat!( - "interface!: unrecognized field tag `", - stringify!($unknown), - "`. Supported tags: WithGetter, WithSetter, WithNotifier." - )); - }; -} - -/// Per-tag wrapper method helper for `interface_consumer_mixed!`. -/// -/// Emits one wrapper method per tag inside the `impl {Id}Consumer` block. -/// - `WithNotifier` - no method generated (subscribe is called directly on the struct field) -/// - `WithGetter` - `pub fn get_{name}<'a>(&'a self) -> impl Future> + 'a` -/// - `WithSetter` - `pub fn set_{name}<'a>(&'a self, value: T) -> impl Future> + 'a` -/// -/// NOTE: Must be called inside a `score_com::paste::paste! { impl ... { HERE } }` block. -#[doc(hidden)] -#[macro_export] -macro_rules! _field_consumer_methods { - // Base: no more tags - ($fi_name:ident, $fi_type:ty, []) => {}; - - // WithNotifier: no method generated - subscribe is on the struct field directly - ($fi_name:ident, $fi_type:ty, [WithNotifier $(, $rest:ident)*]) => { - $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); - }; - - // WithGetter: emit get_{name}() async wrapper (uses [<>] - must be inside paste!) - ($fi_name:ident, $fi_type:ty, [WithGetter $(, $rest:ident)*]) => { - /// Asynchronously get the current value of the field. - /// Returns a future that resolves to `Result>`. - /// Independent of subscription lifecycle - available before and after `subscribe()`. - pub fn []<'a>( - &'a self, - ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { - score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _get>], ()) - } - $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); - }; - - // WithSetter: emit set_{name}(value) async wrapper (uses [<>] - must be inside paste!) - ($fi_name:ident, $fi_type:ty, [WithSetter $(, $rest:ident)*]) => { - /// Asynchronously set the value of the field. - /// Returns a future that resolves to `Result>` - /// containing the confirmed field value from the producer. - /// Independent of subscription lifecycle - available before and after `subscribe()`. - pub fn []<'a>( - &'a self, - value: $fi_type, - ) -> impl core::future::Future::MethodReturnSample<$fi_type>>> + 'a { - score_com::MethodCaller::invoke_with_copy(&self.[<$fi_name _set>], (value,)) - } - $crate::_field_consumer_methods!($fi_name, $fi_type, [$($rest),*]); - }; - - // Unrecognized tag - ($fi_name:ident, $fi_type:ty, [$unknown:ident $(, $rest:ident)*]) => { - compile_error!(concat!( - "interface!: unrecognized field tag `", - stringify!($unknown), - "`. Supported tags: WithGetter, WithSetter, WithNotifier." - )); - }; -} - /// Generates the `{id}Consumer` struct and its `Consumer` trait implementation for /// interfaces that may contain any combination of events, fields, and methods. /// diff --git a/score/mw/com/rust/score_com_concept/interface_producer_macros.rs b/score/mw/com/rust/score_com_concept/interface_producer_macros.rs index 3b441c931..986dcdf93 100644 --- a/score/mw/com/rust/score_com_concept/interface_producer_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_producer_macros.rs @@ -386,7 +386,7 @@ macro_rules! _interface_collect_members { ); }; - // Field member WITHOUT tags: `name : Field ,?` — compile error. + // Field member WITHOUT tags: `name : Field ,?` compile error. ( @id[$_id:ident, $_uid:expr] @ev[$($ev_name:ident : $ev_type:ty ,)*] @@ -839,7 +839,7 @@ mod tests { /// ``` /// mod my_module { /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// WithGetter, WithSetter, WithNotifier}; + /// FieldPublisher, WithGetter, WithSetter, WithNotifier}; /// /// #[derive(Debug, Reloc)] /// #[repr(C)] @@ -873,6 +873,241 @@ mod tests { #[cfg(doctest)] fn interface_macro_mixed() {} + /// Field with `WithNotifier` only consumer can subscribe to value-change notifications. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithNotifier}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field: FieldSubscriber`. + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No `_get` or `_set` callers are generated on the consumer side. + #[cfg(doctest)] + fn interface_macro_field_with_notifier_only() {} + + /// Field with `WithGetter` only consumer can call async `get_*()`. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithGetter}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field_get: FieldGetCaller`. + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No subscriber or `_set` caller is generated on the consumer side. + #[cfg(doctest)] + fn interface_macro_field_with_getter_only() {} + + /// Field with `WithSetter` only consumer can call async `set_*()`. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithSetter}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field_set: FieldSetCaller`. + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No subscriber or `_get` caller is generated on the consumer side. + #[cfg(doctest)] + fn interface_macro_field_with_setter_only() {} + + /// Field with `WithGetter + WithNotifier` consumer can both get and subscribe. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithGetter, WithNotifier}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field: FieldSubscriber` (from `WithNotifier`) + /// and `left_tire_field_get: FieldGetCaller` (from `WithGetter`). + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No `_set` caller is generated. + #[cfg(doctest)] + fn interface_macro_field_with_getter_and_notifier() {} + + /// Field with `WithSetter + WithNotifier` consumer can both set and subscribe. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithSetter, WithNotifier}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field: FieldSubscriber` (from `WithNotifier`) + /// and `left_tire_field_set: FieldSetCaller` (from `WithSetter`). + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No `_get` caller is generated. + #[cfg(doctest)] + fn interface_macro_field_with_setter_and_notifier() {} + + /// Field with `WithGetter + WithSetter` consumer can both get and set, without notifications. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithGetter, WithSetter}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field_get: FieldGetCaller` (from `WithGetter`) + /// and `left_tire_field_set: FieldSetCaller` (from `WithSetter`). + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No subscriber is generated (no `WithNotifier`). + #[cfg(doctest)] + fn interface_macro_field_with_getter_and_setter() {} + + /// Multiple fields with different tag combinations on the same interface. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithGetter, WithSetter, WithNotifier}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// notify_only_field: Field, + /// get_only_field: Field, + /// set_only_field: Field, + /// get_set_field: Field, + /// get_notify_field: Field, + /// set_notify_field: Field, + /// full_field: Field, + /// } + /// ); + /// } + /// ``` + /// Each field generates only the consumer-side accessors for its declared tags: + /// - `notify_only_field`: subscriber only. + /// - `get_only_field`: `_get` caller only. + /// - `set_only_field`: `_set` caller only. + /// - `get_set_field`: `_get` and `_set` callers, no subscriber. + /// - `get_notify_field`: `_get` caller and subscriber, no `_set`. + /// - `set_notify_field`: `_set` caller and subscriber, no `_get`. + /// - `full_field`: subscriber, `_get` caller, and `_set` caller. + /// All fields get a `FieldPublisher` on the producer side regardless of tags. + #[cfg(doctest)] + fn interface_macro_field_all_tag_combinations() {} + + /// Using an unrecognized field tag is a compile-time error. + /// + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// } + /// ``` + /// This fails to compile because `WithReadOnly` is not a recognized field tag. + /// Supported tags are: `WithGetter`, `WithSetter`, `WithNotifier`. + #[cfg(doctest)] + fn interface_macro_field_unrecognized_tag() {} + /// ```compile_fail /// mod my_module { /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; @@ -1259,7 +1494,7 @@ mod validation_tests { const ID: &'static str = "Tire"; } - crate::interface!( + score_com::interface!( interface Vehicle { left_tire: Event, } @@ -1305,7 +1540,7 @@ mod validation_tests { const ID: &'static str = "Exhaust"; } - crate::interface!( + score_com::interface!( interface Vehicle { left_tire: Event, exhaust: Event, @@ -1341,7 +1576,7 @@ mod validation_tests { const ID: &'static str = "Tire"; } - crate::interface!( + score_com::interface!( interface Engine { rpm: Event, } @@ -1372,7 +1607,7 @@ mod validation_tests { const ID: &'static str = "Tire"; } - crate::interface!( + score_com::interface!( interface Transmission { gear: Event, } @@ -1405,7 +1640,7 @@ mod validation_tests { const ID: &'static str = "Tire"; } - crate::interface!( + score_com::interface!( interface Battery, { Id = "com.example.Battery", voltage: Event, @@ -1460,7 +1695,7 @@ mod validation_tests { const ID: &'static str = "Event3Data"; } - crate::interface!( + score_com::interface!( interface MultiEvent { event_one: Event, event_two: Event, @@ -1504,7 +1739,7 @@ mod validation_tests { const ID: &'static str = "Tire"; } - crate::interface!( + score_com::interface!( interface Suspension { travel: Event, } @@ -1546,7 +1781,7 @@ mod validation_tests { const ID: &'static str = "Data"; } - crate::interface!( + score_com::interface!( interface ABS { status: Event, } From 34e66c2e0a5538da8c726dd868228c4bf06e9e13 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Fri, 31 Jul 2026 12:30:11 +0530 Subject: [PATCH 23/25] Rust::com Add get handler in type state pattern and tags for Field * Updated tags for field to generate the field specific methods * Updated intefrca macro as well --- .../com-api-example/src/field_producer.rs | 4 + .../com-api-example/src/mixed_monitor.rs | 8 +- .../interface_producer_macros.rs | 103 ++++++++- score/mw/com/rust/score_com_macros/lib.rs | 2 +- .../score_com_macros/type_state_validator.rs | 217 ++++++++++++++---- 5 files changed, 285 insertions(+), 49 deletions(-) diff --git a/score/mw/com/example/com-api-example/src/field_producer.rs b/score/mw/com/example/com-api-example/src/field_producer.rs index e47a2e560..dcbb06987 100644 --- a/score/mw/com/example/com-api-example/src/field_producer.rs +++ b/score/mw/com/example/com-api-example/src/field_producer.rs @@ -56,6 +56,7 @@ where // Must register handlers and initialize all fields before offer() is available let offered = producer .init() + // Register set-handler callbacks - required before offer() for WithSetter fields .register_set_handler_left_tire(move |val: Tire| { println!("Received tire pressure update: {:?}", val); // Additional logic: inspect or act on the accepted value (logging, telemetry, etc.). @@ -65,6 +66,9 @@ where let _ = val; println!("Received exhaust update"); }) + // Register get-handler callbacks - required before offer() for WithGetter fields + .register_get_handler_left_tire(|| Tire { pressure: 32.0 }) + .register_get_handler_exhaust(|| Exhaust {}) .update_left_tire(initial_tire_value) .expect("Failed to update left_tire field") .update_exhaust(initial_exhaust_value) diff --git a/score/mw/com/example/com-api-example/src/mixed_monitor.rs b/score/mw/com/example/com-api-example/src/mixed_monitor.rs index fb1c319e7..2996da7f5 100644 --- a/score/mw/com/example/com-api-example/src/mixed_monitor.rs +++ b/score/mw/com/example/com-api-example/src/mixed_monitor.rs @@ -29,7 +29,8 @@ // Producer side (skeleton): // - Events are published via `offered.left_tire.send(...)` / `offered.exhaust.send(...)`. // - Fields require an initial value (`update_left_tire_field` / `update_exhaust_field`) and -// a set-handler (`register_set_handler_*_field`) before `offer()` is available +// a set-handler (`register_set_handler_*_field`) and a get-handler +// (`register_get_handler_*_field`) before `offer()` is available // (both enforced at compile time via type state). // - Methods require all handlers to be registered before `offer()` is available (same type state). // @@ -69,6 +70,7 @@ type VehicleMonitorConsumer = ::Consume /// The type-state chain on `init()` enforces at **compile time** that: /// - every Field has an initial value set (`update_*_field`) /// - every Field has a set-handler registered (`register_set_handler_*_field`) +/// - every Field has a get-handler registered (`register_get_handler_*_field`) /// - every Method has a handler registered (`register_*_handler`) /// /// Calling `offer()` before satisfying all of the above is a **compile error**. @@ -97,6 +99,9 @@ where println!("[Producer] set_handler left_tire_field: {:?}", val); // Additional validation or side-effect logic can go here. }) + // Register get-handler: called by the middleware when a consumer calls Get on this field. + // Required before offer() for WithGetter fields. + .register_get_handler_left_tire_field(|| Tire { pressure: 32.0 }) // Set initial field value (required before offer()). .update_left_tire_field(initial_tire) .expect("Failed to set initial value for left_tire_field") @@ -105,6 +110,7 @@ where let _ = val; println!("[Producer] set_handler exhaust_field"); }) + .register_get_handler_exhaust_field(|| Exhaust {}) .update_exhaust_field(initial_exhaust) .expect("Failed to set initial value for exhaust_field") // Method: update_tire_pressure(Tire) -> () diff --git a/score/mw/com/rust/score_com_concept/interface_producer_macros.rs b/score/mw/com/rust/score_com_concept/interface_producer_macros.rs index 986dcdf93..c9b3f2ef4 100644 --- a/score/mw/com/rust/score_com_concept/interface_producer_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_producer_macros.rs @@ -329,6 +329,8 @@ macro_rules! _interface_collect_members { $id, events[$($ev_name : $ev_type ,)*], fields[$($fi_name : $fi_type ,)*], + fields_setter[$($fis_name : $fis_type ,)*], + fields_getter[$($fig_name : $fig_type ,)*], methods[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] ); }; @@ -595,6 +597,8 @@ macro_rules! interface_producer_mixed { $id:ident, events[$($ev_name:ident : $ev_type:ty ,)+], fields[], + fields_setter[], + fields_getter[], methods[] ) => { $crate::interface_producer!($id, $($ev_name, Event<$ev_type>),+); @@ -605,13 +609,20 @@ macro_rules! interface_producer_mixed { $id:ident, events[$($ev_name:ident : $ev_type:ty ,)*], fields[$($fi_name:ident : $fi_type:ty ,)*], + fields_setter[$($fis_name:ident : $fis_type:ty ,)*], + fields_getter[$($fig_name:ident : $fig_type:ty ,)*], methods[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] ) => { score_com::paste::paste! { - // Producer struct - derives TypeStateValidator for compile-time offer() gating. + // Producer struct - derives TypeStateValidator for compile-time `offer()`. // Fields: FieldPublisher per field + MethodHandler per method. - // Event publishers are NOT stored here; they are created during _offer_internal(). + // Event publishers are NOT stored here they are created during _offer_internal(). + // #[field_setter_list] and #[field_getter_list] are derive helper attributes introduced + // by TypeStateValidator. They tell it which fields have WithSetter / WithGetter tags + // so only those fields get the corresponding type-state handler steps. #[derive($crate::score_com_macros::TypeStateValidator)] + #[field_setter_list($($fis_name,)*)] + #[field_getter_list($($fig_name,)*)] pub struct [<$id Producer>] { $( $fi_name: R::FieldPublisher<$fi_type>, @@ -898,6 +909,36 @@ mod tests { /// - `VehicleConsumer` has `left_tire_field: FieldSubscriber`. /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. /// No `_get` or `_set` callers are generated on the consumer side. + /// + /// `WithNotifier`-only fields do not generate a `register_set_handler_*` step: + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, WithNotifier}; + /// #[derive(Debug, Reloc, Clone)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { const ID: &'static str = "Tire"; } + /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); + /// fn _check(p: VehicleProducer) { + /// let _ = p.init().register_set_handler_left_tire_field(|_: Tire| {}); + /// } + /// } + /// ``` + /// + /// `WithNotifier`-only fields do not generate a `register_get_handler_*` step: + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, WithNotifier}; + /// #[derive(Debug, Reloc, Clone)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { const ID: &'static str = "Tire"; } + /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); + /// fn _check(p: VehicleProducer) { + /// let _ = p.init().register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); + /// } + /// } + /// ``` #[cfg(doctest)] fn interface_macro_field_with_notifier_only() {} @@ -906,7 +947,8 @@ mod tests { /// ``` /// mod my_module { /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// FieldPublisher, WithGetter}; + /// FieldPublisher, WithGetter, + /// LolaRuntimeImpl as LolaRuntime}; /// /// #[derive(Debug, Reloc)] /// #[repr(C)] @@ -920,12 +962,33 @@ mod tests { /// left_tire_field: Field, /// } /// ); + /// + /// // Compile-time check `register_get_handler_*` exists for WithGetter fields. + /// #[allow(dead_code)] + /// fn _check_get_handler(p: VehicleProducer) { + /// let _ = p.init().register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); + /// } /// } /// ``` /// Generates: /// - `VehicleConsumer` has `left_tire_field_get: FieldGetCaller`. /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. /// No subscriber or `_set` caller is generated on the consumer side. + /// + /// `WithGetter`-only fields do not generate a `register_set_handler_*` step: + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, WithGetter}; + /// #[derive(Debug, Reloc, Clone)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { const ID: &'static str = "Tire"; } + /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); + /// fn _check(p: VehicleProducer) { + /// let _ = p.init().register_set_handler_left_tire_field(|_: Tire| {}); + /// } + /// } + /// ``` #[cfg(doctest)] fn interface_macro_field_with_getter_only() {} @@ -934,7 +997,8 @@ mod tests { /// ``` /// mod my_module { /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// FieldPublisher, WithSetter}; + /// FieldPublisher, WithSetter, + /// LolaRuntimeImpl as LolaRuntime}; /// /// #[derive(Debug, Reloc)] /// #[repr(C)] @@ -948,12 +1012,33 @@ mod tests { /// left_tire_field: Field, /// } /// ); + /// + /// // Compile-time check `register_set_handler_*` exists for WithSetter fields. + /// #[allow(dead_code)] + /// fn _check_set_handler(p: VehicleProducer) { + /// let _ = p.init().register_set_handler_left_tire_field(|_: Tire| {}); + /// } /// } /// ``` /// Generates: /// - `VehicleConsumer` has `left_tire_field_set: FieldSetCaller`. /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. /// No subscriber or `_get` caller is generated on the consumer side. + /// + /// `WithSetter`-only fields do not generate a `register_get_handler_*` step: + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, WithSetter}; + /// #[derive(Debug, Reloc, Clone)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { const ID: &'static str = "Tire"; } + /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); + /// fn _check(p: VehicleProducer) { + /// let _ = p.init().register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); + /// } + /// } + /// ``` #[cfg(doctest)] fn interface_macro_field_with_setter_only() {} @@ -1020,7 +1105,8 @@ mod tests { /// ``` /// mod my_module { /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// FieldPublisher, WithGetter, WithSetter}; + /// FieldPublisher, WithGetter, WithSetter, + /// LolaRuntimeImpl as LolaRuntime}; /// /// #[derive(Debug, Reloc)] /// #[repr(C)] @@ -1034,6 +1120,13 @@ mod tests { /// left_tire_field: Field, /// } /// ); + /// + /// // Compile-time proof: both handler methods exist for WithGetter + WithSetter fields. + /// #[allow(dead_code)] + /// fn _assert_both_handlers(p: VehicleProducer) { + /// let v = p.init().register_set_handler_left_tire_field(|_: Tire| {}); + /// let _ = v.register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); + /// } /// } /// ``` /// Generates: diff --git a/score/mw/com/rust/score_com_macros/lib.rs b/score/mw/com/rust/score_com_macros/lib.rs index 0edf50166..cd3d9f08e 100644 --- a/score/mw/com/rust/score_com_macros/lib.rs +++ b/score/mw/com/rust/score_com_macros/lib.rs @@ -388,7 +388,7 @@ fn collect_field_types(data: &Data) -> Result, ()> { /// ``` // TODO: Document tests need to be added for this macro, including successful and failed compilation cases. // Once field or method design merged, other PR can add the tests for this macro. -#[proc_macro_derive(TypeStateValidator)] +#[proc_macro_derive(TypeStateValidator, attributes(field_setter_list, field_getter_list))] pub fn derive_typestate_validator(input: TokenStream) -> TokenStream { type_state_validator::derive_typestate_validator_impl(input) } diff --git a/score/mw/com/rust/score_com_macros/type_state_validator.rs b/score/mw/com/rust/score_com_macros/type_state_validator.rs index e6d0dd223..4b953f859 100644 --- a/score/mw/com/rust/score_com_macros/type_state_validator.rs +++ b/score/mw/com/rust/score_com_macros/type_state_validator.rs @@ -13,27 +13,65 @@ use proc_macro::TokenStream; use quote::quote; +use std::collections::HashSet; use syn::{parse_macro_input, Data, DeriveInput, Fields, Type}; +/// Parse a struct-level attribute of the form `#[attr_name(ident1, ident2, ...)]` +/// and return the set of identifier strings it contains. +/// Returns an empty set if the attribute is absent or its argument list is empty. +/// This is for FieldPublisher WithSetter / WithGetter capability tags, which are emitted by the interface_producer_mixed! macro. +fn parse_name_list_attr(attrs: &[syn::Attribute], attr_name: &str) -> HashSet { + let mut names = HashSet::new(); + for attr in attrs { + if attr.path().is_ident(attr_name) { + if let Ok(list) = attr.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + ) { + for ident in list { + names.insert(ident.to_string()); + } + } + } + } + names +} + /// Unified type-state validator for producers containing `FieldPublisher` and/or /// `MethodHandler` members. /// /// Detects member type by the last segment of each field's type path: -/// - `FieldPublisher` - generates `update_{name}()` (Uninit - Init) and -/// `register_set_handler_{name}()` (HandlerNotSet - HandlerSet) per member. +/// - `FieldPublisher` - generates `update_{name}()` (Uninit-Init) per member. +/// Additionally generates `register_set_handler_{name}()` (HandlerNotSet-HandlerSet) +/// for fields listed in `#[field_setter_list(...)]`, and +/// `register_get_handler_{name}()` (HandlerNotSet-HandlerSet) for fields listed in +/// `#[field_getter_list(...)]`. /// - `MethodHandler` - generates `register_{name}_handler()` /// (HandlerNotSet - HandlerSet) per member. /// - `instance_info` field is always skipped. /// +/// # Struct-level helper attributes (declared by the `TypeStateValidator` derive) +/// +/// - `#[field_setter_list(name1, name2, ...)]` - comma-separated field names that have +/// the `WithSetter` capability tag. Only these fields get a `register_set_handler_*` +/// type-state step and an `Hi` generic parameter. +/// - `#[field_getter_list(name1, name2, ...)]` - comma-separated field names that have +/// the `WithGetter` capability tag. Only these fields get a `register_get_handler_*` +/// type-state step and a `Gi` generic parameter. +/// +/// Both attributes are emitted by `interface_producer_mixed!` based on the capability +/// tags declared in the `interface!` macro invocation. +/// /// # Generated validator struct /// -/// `{Name}Validator` where: -/// - `Si` tracks update state of field member `i` (`Uninit` / `Init`) -/// - `Hi` tracks set-handler state of field member `i` (`HandlerNotSet` / `HandlerSet`) -/// - `Mj` tracks handler state of method member `j` (`HandlerNotSet` / `HandlerSet`) +/// `{Name}Validator` where: +/// - `Si` tracks update state of field member `i` (`Uninit` / `Init`) - ALL fields +/// - `Hj` tracks set-handler state of setter field `j` (`HandlerNotSet` / `HandlerSet`) - WithSetter fields only +/// - `Gk` tracks get-handler state of getter field `k` (`HandlerNotSet` / `HandlerSet`) - WithGetter fields only +/// - `Mp` tracks handler state of method member `p` (`HandlerNotSet` / `HandlerSet`) /// -/// `offer()` is only generated for the impl where ALL `Si = Init`, ALL `Hi = HandlerSet`, -/// ALL `Mj = HandlerSet`. It calls `_offer_internal()` on the wrapped producer. +/// `offer()` is only generated for the impl where ALL `Si = Init`, ALL `Hj = HandlerSet`, +/// ALL `Gk = HandlerSet`, ALL `Mp = HandlerSet`. It calls `_offer_internal()` on the +/// wrapped producer. /// /// Entry point on the producer: `init()` - returns the validator with every state /// parameter set to its initial value (`Uninit` / `HandlerNotSet`). @@ -58,6 +96,11 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { (quote! { R }, quote! { R: score_com::Runtime + ?Sized }) }; + // Read #[field_setter_list(name1, name2, ...)] and #[field_getter_list(name1, name2, ...)] + // emitted by interface_producer_mixed! to know which fields have WithSetter / WithGetter. + let setter_names = parse_name_list_attr(&input.attrs, "field_setter_list"); + let getter_names = parse_name_list_attr(&input.attrs, "field_getter_list"); + let fields = match &input.data { Data::Struct(data) => match &data.fields { Fields::Named(fields) => &fields.named, @@ -84,6 +127,8 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { struct FieldMember { ident: syn::Ident, inner_ty: Type, // T extracted from FieldPublisher + has_setter: bool, + has_getter: bool, } struct MethodMember { ident: syn::Ident, @@ -112,7 +157,10 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { "FieldPublisher" => { if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { + let name_str = ident.to_string(); field_members.push(FieldMember { + has_setter: setter_names.contains(&name_str), + has_getter: getter_names.contains(&name_str), ident, inner_ty: inner.clone(), }); @@ -156,42 +204,70 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { let validator_name = syn::Ident::new(&format!("{}Validator", name), name.span()); // State param naming: - // S{i} — update state for field member i (Uninit / Init) - // H{i} — set-handler state for field member i (HandlerNotSet / HandlerSet) - // M{j} — handler state for method member j (HandlerNotSet / HandlerSet) - // Combined order in the validator struct: [S0..Sn, H0..Hn, M0..Mm] + // S{i} - update state for field member i (Uninit / Init) - ALL fields + // H{j} - set-handler state for setter field j (HandlerNotSet / HandlerSet) - WithSetter only + // G{k} - get-handler state for getter field k (HandlerNotSet / HandlerSet) - WithGetter only + // M{p} - handler state for method member p (HandlerNotSet / HandlerSet) + // Combined order in the validator struct: [S0..Sn, H0..Hm, G0..Gk, M0..Mp] + let field_update_params: Vec = (0..field_members.len()) .map(|i| syn::Ident::new(&format!("S{}", i), proc_macro::Span::call_site().into())) .collect(); - let field_handler_params: Vec = (0..field_members.len()) - .map(|i| syn::Ident::new(&format!("H{}", i), proc_macro::Span::call_site().into())) + + // Collect setter/getter subsets once to avoid repeated filtering. + let setter_field_indices: Vec = field_members + .iter() + .enumerate() + .filter(|(_, m)| m.has_setter) + .map(|(i, _)| i) + .collect(); + let getter_field_indices: Vec = field_members + .iter() + .enumerate() + .filter(|(_, m)| m.has_getter) + .map(|(i, _)| i) + .collect(); + + let field_setter_params: Vec = (0..setter_field_indices.len()) + .map(|j| syn::Ident::new(&format!("H{}", j), proc_macro::Span::call_site().into())) + .collect(); + let field_getter_params: Vec = (0..getter_field_indices.len()) + .map(|k| syn::Ident::new(&format!("G{}", k), proc_macro::Span::call_site().into())) .collect(); let method_handler_params: Vec = (0..method_members.len()) - .map(|j| syn::Ident::new(&format!("M{}", j), proc_macro::Span::call_site().into())) + .map(|p| syn::Ident::new(&format!("M{}", p), proc_macro::Span::call_site().into())) .collect(); - // Flat list used in struct definition and impl generics: [S0..Sn, H0..Hn, M0..Mm] + // Flat list used in struct definition and impl generics: + // [S0..Sn, H0..Hm, G0..Gk, M0..Mp] let all_params: Vec<&syn::Ident> = field_update_params .iter() - .chain(field_handler_params.iter()) + .chain(field_setter_params.iter()) + .chain(field_getter_params.iter()) .chain(method_handler_params.iter()) .collect(); + let n_fields = field_members.len(); + let n_setters = setter_field_indices.len(); + let n_getters = getter_field_indices.len(); + // Initial states for init() entry point. - let init_states: Vec<_> = (0..field_members.len()) + let init_states: Vec<_> = (0..n_fields) .map(|_| quote! { ::score_com::Uninit }) - .chain((0..field_members.len()).map(|_| quote! { ::score_com::HandlerNotSet })) + .chain((0..n_setters).map(|_| quote! { ::score_com::HandlerNotSet })) + .chain((0..n_getters).map(|_| quote! { ::score_com::HandlerNotSet })) .chain((0..method_members.len()).map(|_| quote! { ::score_com::HandlerNotSet })) .collect(); // All-satisfied states required by offer(). - let done_states: Vec<_> = (0..field_members.len()) + let done_states: Vec<_> = (0..n_fields) .map(|_| quote! { ::score_com::Init }) - .chain((0..field_members.len()).map(|_| quote! { ::score_com::HandlerSet })) + .chain((0..n_setters).map(|_| quote! { ::score_com::HandlerSet })) + .chain((0..n_getters).map(|_| quote! { ::score_com::HandlerSet })) .chain((0..method_members.len()).map(|_| quote! { ::score_com::HandlerSet })) .collect(); - // update_{name}() impls for each field member + // update_{name}() impls for each field member. // Transitions Si: Uninit - Init while all other state params stay generic. let update_methods: Vec<_> = field_members .iter() @@ -234,26 +310,27 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { }) .collect(); - // register_set_handler_{name}() impls for each field member - // Hi is at index field_members.len() + i in all_params. - // Transitions Hi: HandlerNotSet - HandlerSet while all other state params stay generic. - let register_set_handler_methods: Vec<_> = field_members + // register_set_handler_{name}() impls - only for WithSetter fields. + // H{j} is at index n_fields + j in all_params. + // Transitions Hj: HandlerNotSet - HandlerSet while all other state params stay generic. + let register_set_handler_methods: Vec<_> = setter_field_indices .iter() .enumerate() - .map(|(i, member)| { + .map(|(j, &field_idx)| { + let member = &field_members[field_idx]; let register_fn = syn::Ident::new( &format!("register_set_handler_{}", member.ident), member.ident.span(), ); let inner_ty = &member.inner_ty; let field_ident = &member.ident; - let hi_index = field_members.len() + i; + let hj_index = n_fields + j; let after: Vec<_> = all_params .iter() .enumerate() .map(|(k, p)| { - if k == hi_index { + if k == hj_index { quote! { ::score_com::HandlerSet } } else { quote! { #p } @@ -285,13 +362,65 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { }) .collect(); - // register_{name}_handler() impls for each method member - // Mj is at index 2 * field_members.len() + j in all_params. - // Transitions Mj: HandlerNotSet - HandlerSet while all other state params stay generic. + // register_get_handler_{name}() impls - only for WithGetter fields. + // G{k} is at index n_fields + n_setters + k in all_params. + // Transitions Gk: HandlerNotSet - HandlerSet while all other state params stay generic. + let register_get_handler_methods: Vec<_> = getter_field_indices + .iter() + .enumerate() + .map(|(k, &field_idx)| { + let member = &field_members[field_idx]; + let register_fn = syn::Ident::new( + &format!("register_get_handler_{}", member.ident), + member.ident.span(), + ); + let inner_ty = &member.inner_ty; + let field_ident = &member.ident; + let gk_index = n_fields + n_setters + k; + + let after: Vec<_> = all_params + .iter() + .enumerate() + .map(|(k2, p)| { + if k2 == gk_index { + quote! { ::score_com::HandlerSet } + } else { + quote! { #p } + } + }) + .collect(); + + quote! { + impl<#runtime_param_with_bounds, #(#all_params),*> + #validator_name<#runtime_param_name, #(#all_params),*> + where + <#runtime_param_name as score_com::Runtime>::FieldPublisher<#inner_ty>: Send, + { + pub fn #register_fn( + self, + handler: F, + ) -> #validator_name<#runtime_param_name, #(#after),*> + where + F: Fn() -> #inner_ty + Send + 'static, + { + self.producer.#field_ident.register_get_handler(handler); + #validator_name { + producer: self.producer, + _phantom: core::marker::PhantomData, + } + } + } + } + }) + .collect(); + + // register_{name}_handler() impls for each method member. + // M{p} is at index n_fields + n_setters + n_getters + p in all_params. + // Transitions Mp: HandlerNotSet - HandlerSet while all other state params stay generic. let register_handler_methods: Vec<_> = method_members .iter() .enumerate() - .map(|(j, member)| { + .map(|(p, member)| { let register_fn = syn::Ident::new( &format!("register_{}_handler", member.ident), member.ident.span(), @@ -299,16 +428,16 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { let args_ty = &member.args_ty; let return_ty = &member.return_ty; let method_ident = &member.ident; - let mj_index = 2 * field_members.len() + j; + let mp_index = n_fields + n_setters + n_getters + p; let after: Vec<_> = all_params .iter() .enumerate() - .map(|(k, p)| { - if k == mj_index { + .map(|(k, p_param)| { + if k == mp_index { quote! { ::score_com::HandlerSet } } else { - quote! { #p } + quote! { #p_param } } }) .collect(); @@ -340,22 +469,26 @@ pub fn derive_typestate_validator_impl(input: TokenStream) -> TokenStream { let expanded = quote! { // Validator struct type params track state of every Field and Method member. - // Layout: + // Layout: pub struct #validator_name<#runtime_param_with_bounds, #(#all_params),*> { producer: #name<#runtime_param_name>, _phantom: core::marker::PhantomData<(#(#all_params,)*)>, } - // update_{name}() - transitions Si: Uninit - Init + // update_{name}() - transitions Si: Uninit - Init (all fields) #(#update_methods)* - // register_set_handler_{name}() - transitions Hi: HandlerNotSet - HandlerSet + // register_set_handler_{name}() - transitions Hj: HandlerNotSet - HandlerSet (WithSetter fields only) #(#register_set_handler_methods)* - // register_{name}_handler() - transitions Mj: HandlerNotSet - HandlerSet + // register_get_handler_{name}() - transitions Gk: HandlerNotSet - HandlerSet (WithGetter fields only) + #(#register_get_handler_methods)* + + // register_{name}_handler() - transitions Mp: HandlerNotSet - HandlerSet (methods) #(#register_handler_methods)* - // offer() is only available when ALL Si = Init, ALL Hi = HandlerSet, ALL Mj = HandlerSet. + // offer() is only available when ALL Si = Init, ALL Hj = HandlerSet, + // ALL Gk = HandlerSet, ALL Mp = HandlerSet. impl<#runtime_param_with_bounds> #validator_name<#runtime_param_name, #(#done_states),*> { From 79078701948e7a48a75a203eaf9e7ddae631d9f1 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Fri, 31 Jul 2026 12:53:44 +0530 Subject: [PATCH 24/25] Rust::com Field Design diagram and document * Created trait level document design * document for field desing --- .../com-api-gen/com_api_gen.rs | 5 +- .../com-api-example/src/field_consumer.rs | 4 +- .../com-api-example/src/method_consumer.rs | 2 +- .../com-api-example/src/mixed_monitor.rs | 38 +- .../com-api/com-api-runtime-lola/consumer.rs | 2 +- .../rust/com-api/com-api-runtime-lola/lib.rs | 2 - .../com-api/com-api-runtime-lola/method.rs | 13 +- .../com-api/com-api-runtime-lola/producer.rs | 2 +- .../com-api/com-api-runtime-lola/runtime.rs | 2 +- .../com/rust/design/design_document_field.md | 456 ++++++++++++++++++ score/mw/com/rust/design/field_overview.puml | 39 ++ score/mw/com/rust/design/field_overview.svg | 1 + .../com/rust/design/field_trait_diagram.puml | 175 +++++++ .../com/rust/design/field_trait_diagram.svg | 1 + .../mw/com/rust/score_com_concept/concept.rs | 2 +- .../rust/score_com_concept/field_concept.rs | 10 +- .../method_arities_macros.rs | 2 +- 17 files changed, 710 insertions(+), 46 deletions(-) create mode 100644 score/mw/com/rust/design/design_document_field.md create mode 100644 score/mw/com/rust/design/field_overview.puml create mode 100644 score/mw/com/rust/design/field_overview.svg create mode 100644 score/mw/com/rust/design/field_trait_diagram.puml create mode 100644 score/mw/com/rust/design/field_trait_diagram.svg diff --git a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs index aed2e73db..b13987094 100644 --- a/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs +++ b/score/mw/com/example/com-api-example/com-api-gen/com_api_gen.rs @@ -73,9 +73,8 @@ interface!( // Field-based interface with compile-time initialization safety. // All fields must be explicitly initialized via the Type State pattern before offering. // The Type State pattern ensures that you cannot call offer() until all fields have been updated. -// Just for demonstration of APIs usage we are creating a separate interface for field, -// we have plan to update the interface macro to support mixed event and field interface in future. -// https://github.com/eclipse-score/communication/issues/701 +// This separate field-only interface is intentionally kept for demonstration purposes. +// Mixed event and field interfaces are already supported, as shown by VehicleMonitor below. interface!( interface VehicleField { Id = "VehicleFieldInterface", diff --git a/score/mw/com/example/com-api-example/src/field_consumer.rs b/score/mw/com/example/com-api-example/src/field_consumer.rs index dc7047d7a..e423717bb 100644 --- a/score/mw/com/example/com-api-example/src/field_consumer.rs +++ b/score/mw/com/example/com-api-example/src/field_consumer.rs @@ -20,8 +20,8 @@ // as this are not part of any callable because of that unused warning is suppressed for this file. use score_com::{ - Builder, FindServiceSpecifier, InstanceSpecifier, - Interface, Runtime, SampleContainer, ServiceDiscovery, Subscriber, Subscription, + Builder, FindServiceSpecifier, InstanceSpecifier, Interface, Runtime, SampleContainer, + ServiceDiscovery, Subscriber, Subscription, }; use com_api_gen::{Tire, VehicleFieldInterface}; diff --git a/score/mw/com/example/com-api-example/src/method_consumer.rs b/score/mw/com/example/com-api-example/src/method_consumer.rs index 7fb0b9390..103b5ef2e 100644 --- a/score/mw/com/example/com-api-example/src/method_consumer.rs +++ b/score/mw/com/example/com-api-example/src/method_consumer.rs @@ -61,7 +61,7 @@ fn create_consumer_method( // Copy path: single positional argument. // Demonstrates calling a method with a single argument, where the argument is copied into the method call. -// Zero-copy path: allocate, write, then call the method with allocaed args. +// Zero-copy path: allocate, write, then call the method with allocated args. #[allow(dead_code)] async fn consumer_method_processing(consumer: VehicleMethodConsumer) { // Copy path: single positional argument — no tuple needed. diff --git a/score/mw/com/example/com-api-example/src/mixed_monitor.rs b/score/mw/com/example/com-api-example/src/mixed_monitor.rs index 2996da7f5..aa03bdee1 100644 --- a/score/mw/com/example/com-api-example/src/mixed_monitor.rs +++ b/score/mw/com/example/com-api-example/src/mixed_monitor.rs @@ -51,7 +51,7 @@ use score_com::{ use com_api_gen::{Exhaust, Tire, VehicleMonitorInterface}; -// Type aliases +// Type aliases #[allow(dead_code)] type VehicleMonitorProducer = ::Producer; @@ -63,7 +63,7 @@ type VehicleMonitorOfferedProducer = #[allow(dead_code)] type VehicleMonitorConsumer = ::Consumer; -// Producer +// Producer /// Create and offer a VehicleMonitor producer. /// @@ -92,7 +92,7 @@ where producer .init() - // Field: left_tire_field + // Field: left_tire_field // Register set-handler: called by the middleware when a consumer calls Set on this field. // Receives the accepted value by value for inspection / side effects. .register_set_handler_left_tire_field(|val: Tire| { @@ -105,7 +105,7 @@ where // Set initial field value (required before offer()). .update_left_tire_field(initial_tire) .expect("Failed to set initial value for left_tire_field") - // Field: exhaust_field + // Field: exhaust_field .register_set_handler_exhaust_field(|val: Exhaust| { let _ = val; println!("[Producer] set_handler exhaust_field"); @@ -113,18 +113,18 @@ where .register_get_handler_exhaust_field(|| Exhaust {}) .update_exhaust_field(initial_exhaust) .expect("Failed to set initial value for exhaust_field") - // Method: update_tire_pressure(Tire) -> () + // Method: update_tire_pressure(Tire) -> () .register_update_tire_pressure_handler(|tire: Tire| { println!("[Producer] update_tire_pressure called: {:?}", tire); }) - // Method: update_front_tires_pressure(Tire, Tire) -> () + // Method: update_front_tires_pressure(Tire, Tire) -> () .register_update_front_tires_pressure_handler(|tire1: Tire, tire2: Tire| { println!( "[Producer] update_front_tires_pressure called: {:?}, {:?}", tire1, tire2 ); }) - // Method: get_tire_pressure() -> Tire + // Method: get_tire_pressure() -> Tire .register_get_tire_pressure_handler(|| { println!("[Producer] get_tire_pressure called"); // Return the current field value; in a real implementation this would @@ -168,7 +168,7 @@ fn update_fields(offered: &VehicleMonitorOfferedProducer) { .expect("Failed to update exhaust_field"); } -// Consumer +// Consumer /// Create a VehicleMonitor consumer by discovering the service instance. #[allow(dead_code)] @@ -176,8 +176,8 @@ fn create_monitor_consumer( runtime: &R, service_id: InstanceSpecifier, ) -> VehicleMonitorConsumer { - let discovery = runtime - .find_service::(FindServiceSpecifier::Specific(service_id)); + let discovery = + runtime.find_service::(FindServiceSpecifier::Specific(service_id)); let instances = discovery .get_available_instances() @@ -217,8 +217,8 @@ async fn consume_monitor(consumer: VehicleMonitorConsumer) { // fn subscribe(&mut self, max_num_samples: usize) -> Result // With &mut self, no field is ever moved out, so consumer remains fully usable in any order. // With this change, unsubscribe return also need to change. - - // Fields (async get/set) + + // Fields (async get/set) // Async get uses MethodCaller<(), Tire> under the hood. match consumer.get_left_tire_field().await { Ok(result) => println!("[Consumer] left_tire_field get: {:?}", *result), @@ -232,7 +232,7 @@ async fn consume_monitor(consumer: VehicleMonitorConsumer) { Err(e) => eprintln!("[Consumer] left_tire_field set failed: {:?}", e), } - // Methods + // Methods // Copy path: single argument. match consumer.update_tire_pressure(Tire { pressure: 30.0 }).await { Ok(_) => println!("[Consumer] update_tire_pressure OK"), @@ -256,7 +256,10 @@ async fn consume_monitor(consumer: VehicleMonitorConsumer) { let tire_ptr = uninit.write(Tire { pressure: 35.0 }); match consumer.update_tire_pressure(tire_ptr).await { Ok(_) => println!("[Consumer] update_tire_pressure (zero-copy) OK"), - Err(e) => eprintln!("[Consumer] update_tire_pressure (zero-copy) failed: {:?}", e), + Err(e) => eprintln!( + "[Consumer] update_tire_pressure (zero-copy) failed: {:?}", + e + ), } // Zero-argument method returning a value. @@ -265,7 +268,7 @@ async fn consume_monitor(consumer: VehicleMonitorConsumer) { Err(e) => eprintln!("[Consumer] get_tire_pressure failed: {:?}", e), } - // Events + // Events // subscribe(self) moves consumer.left_tire out of consumer (partial move). // Whole-struct &self methods are not allowed after this point, but direct // field access to other fields (e.g. consumer.left_tire_field below) still works. @@ -290,7 +293,7 @@ async fn consume_monitor(consumer: VehicleMonitorConsumer) { // event_subscription dropped here (unsubscribed). } - // Fields (subscribe for notifications) + // Fields (subscribe for notifications) // consumer.left_tire is partially moved above, but consumer.left_tire_field is a // distinct field and is still valid Rust tracks field moves individually. { @@ -312,11 +315,10 @@ async fn consume_monitor(consumer: VehicleMonitorConsumer) { // field_subscription dropped here (unsubscribed). } - // TODO: Uncomment when Runtime implementation is ready and + // TODO: Uncomment when Runtime implementation is ready and // subscribe() is changed to take &mut self (no partial move). // match consumer.update_tire_pressure(Tire { pressure: 30.0 }).await { // Ok(_) => println!("[Consumer] update_tire_pressure OK"), // Err(e) => eprintln!("[Consumer] update_tire_pressure failed: {:?}", e), // } - } diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/consumer.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/consumer.rs index 99aa7e688..3650d2f95 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/consumer.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/consumer.rs @@ -29,8 +29,8 @@ //TODO: revist this once com-api is stable - Ticket-234827 #![allow(clippy::needless_lifetimes)] -use crate::Debug; use core::clone::Clone; +use core::fmt::Debug; use core::future::Future; use core::marker::PhantomData; use core::mem::ManuallyDrop; diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs index 0ea777c35..bee0e2f6f 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs @@ -25,8 +25,6 @@ //! The crate is structured to facilitate easy integration and usage of the Lola middleware within applications //! that utilize the COM API abstractions. -use core::fmt::Debug; - mod consumer; // Note: The `method` module is currently a placeholder and // will be implemented in the future for the Lola runtime. diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs index 4f6aefc8c..c2b73f859 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs @@ -189,7 +189,9 @@ impl MethodCaller<(), T, R> for LolaFieldGetCal where (): MethodArgsPtrTuple, { - async move { todo!("Implement zero-copy invoke for LolaFieldGetCaller if C++ side support is available") } + async move { + todo!("Implement zero-copy invoke for LolaFieldGetCaller if C++ side support is available") + } } } @@ -217,9 +219,7 @@ impl MethodCaller<(T,), T, R> for LolaFieldSetC async move { todo!("Implement field set via MethodType::kSet") } } - fn allocate( - &self, - ) -> Result<<(T,) as MethodArgsAllocate>::UninitTuple> + fn allocate(&self) -> Result<<(T,) as MethodArgsAllocate>::UninitTuple> where (T,): MethodArgsAllocate, { @@ -233,7 +233,8 @@ impl MethodCaller<(T,), T, R> for LolaFieldSetC where (T,): MethodArgsPtrTuple, { - async move { todo!("Implement zero-copy invoke for LolaFieldSetCaller if C++ side support is available") } + async move { + todo!("Implement zero-copy invoke for LolaFieldSetCaller if C++ side support is available") + } } } - diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/producer.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/producer.rs index 6c2211954..371ee8bc0 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/producer.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/producer.rs @@ -29,7 +29,7 @@ //TODO: revist this once com-api is stable - Ticket-234827 #![allow(clippy::needless_lifetimes)] -use crate::Debug; +use core::fmt::Debug; use core::marker::PhantomData; use core::mem::ManuallyDrop; use core::ops::{Deref, DerefMut}; diff --git a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs index 0cc67c93c..3ac16156c 100644 --- a/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/runtime.rs @@ -11,7 +11,7 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -use crate::Debug; +use core::fmt::Debug; use core::marker::PhantomData; use std::path::{Path, PathBuf}; diff --git a/score/mw/com/rust/design/design_document_field.md b/score/mw/com/rust/design/design_document_field.md new file mode 100644 index 000000000..cd331a477 --- /dev/null +++ b/score/mw/com/rust/design/design_document_field.md @@ -0,0 +1,456 @@ + +# COM API-Field Design + +This document describes the design of the **field** APIs and usage of it. + +## Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Core Trait Design](#core-trait-design) + - [Runtime-Implemented Traits](#runtime-implemented-traits) + - [Zero-Copy Publish Traits](#zero-copy-publish-traits) + - [Base Subscription Traits](#base-subscription-traits) +- [Capability Tags](#capability-tags) +- [Get and Set Call Paths](#get-and-set-call-paths) +- [Interface Macro Integration](#interface-macro-integration) +- [Type-State Validator](#type-state-validator) +- [Producer Side API Usage](#producer-side-api-usage) +- [Consumer Side API Usage](#consumer-side-api-usage) +- [TODOs and Improvements](#todos-and-improvements) + +--- + +## Overview + +Rust Communication library provides the Field based communication pattern (mostly with alignment of C++ APIs). A field is a named, typed value that lives on a service provider and can be read, written, and subscribed to by consumers. The following are the major points of the design: + +- A field combines three orthogonal capabilities, each activated by a tag in the `interface!` macro: **`WithGetter`** (async get), **`WithSetter`** (async set), and **`WithNotifier`** (subscribe to value-change notifications like Event). +- Field get and set on the consumer side are modelled as `MethodCaller`-based callers, reusing the full Method infrastructure (async futures, `MethodReturnSample`, copy path) without a separate field-specific method design. +- Field publish on the producer side uses a dedicated `FieldPublisher` trait that provides `update()` (copy path) and `allocate()` + `FieldSampleMut::update()` (zero-copy path). +- Handler registration on the producer side is enforced at compile time via the type-state validator: `register_set_handler_{name}` must be called for every `WithSetter` field, `register_get_handler_{name}` must be called for every `WithGetter` field, and the field's initial value must be set via `update_{name}()` for every field, all before `offer()` becomes available. Bypassing the validator and calling `offer()` directly will panic at runtime. + +Fields are defined as part of an interface via the `interface!` macro alongside events and methods: + +```rust +interface!( + interface VehicleField { + Id = "VehicleFieldInterface", + left_tire: Field, + exhaust: Field, + } +); +``` + +The macro uses `name: Field` syntax. At least one capability tag is required; `Field` without tags is a compile error. + +--- + +## Architecture + +The field feature follows the same layered architecture as the rest of the COM API. + +![Field Overview](field_overview.svg) + +> Source: [field_overview](field_overview.svg) + +| Layer | Role | +|-------|------| +| **Application** | User code calls `consumer.get_left_tire().await`, `consumer.set_left_tire(val).await`, subscribes via `consumer.left_tire.subscribe(n)`, and on the producer side calls `producer.init().update_left_tire(...).register_set_handler_left_tire(fn).offer()` | +| **Abstraction** | Platform-independent field traits in `score_com_concept`, `interface!` macro generates typed wrappers, `type_state_validator` enforces compile-time correctness | +| **Runtime** | Concrete `LolaFieldSubscriber` / `LolaFieldSubscription` / `LolaFieldPublisher` / `LolaFieldGetCaller` / `LolaFieldSetCaller` in `com-api-runtime-lola` | +| **FFI** | Rust–C++ bindings bridging field get/set dispatch, handler registration, and subscribe/notify to the underlying middleware | + +--- + +## Core Trait Design + +The full trait diagram is shown below. Source: [field_trait_diagram](field_trait_diagram.svg). + +![Field Trait Diagram](field_trait_diagram.svg) + + +### Runtime-Implemented Traits + +These traits must be implemented by every runtime (e.g. `com-api-runtime-lola`). + +#### `FieldSubscriber` + +Marker supertrait of `Subscriber`. It adds the constraint that the `Subscription` associated type must be a `FieldSubscription`. All concrete subscription APIs (`new`, `subscribe`) are inherited from `Subscriber`. + +```rust +pub trait FieldSubscriber: + concept::Subscriber> +{ +} +``` + +The `interface!` macro generates a `{name}: R::FieldSubscriber` struct field on the consumer for every `WithNotifier` field. The consumer calls `.subscribe(max_num_samples)` on it to receive field-value-change notifications. + +#### `FieldSubscription` + +Extends `Subscription` with two additional query methods for buffer introspection. + +```rust +pub trait FieldSubscription: + concept::Subscription +{ + fn get_num_new_samples_available(&self) -> Result; + fn get_free_sample_count(&self) -> Result; +} +``` + +`get_num_new_samples_available()` reports how many new samples `try_receive` would deliver. `get_free_sample_count()` reports how many more samples can be buffered before the subscription count overflows. + +The base `Subscription` already provides `try_receive`, `receive`, `cancellable_receive`, and `to_stream`. + +#### `FieldPublisher` + +Producer-side field owner. Provides both the copy publish path and the zero-copy publish path, plus set/get handler registration. + +```rust +pub trait FieldPublisher { + type SampleMaybeUninit<'a>: SampleMaybeUninit> + 'a + where + Self: 'a; + + fn new(identifier: &'static str, instance_info: R::ProviderInfo) -> Result + where + Self: Sized; + + fn allocate(&self) -> Result>; + + fn update(&self, value: T) -> Result<()>; + + fn register_set_handler(&self, callback: impl Fn(T) + Send + 'static); + + fn register_get_handler(&self, callback: impl Fn() -> T + Send + 'static); +} +``` + +`update()` is the copy-path publish. For zero-copy writes, use `allocate()` to get an uninitialised slot and then call `write()` and then `update()`. + +`register_set_handler` is called by the middleware whenever a consumer calls `set_{name}()`. It is required in the producer `init()` chain and tracked by the type-state validator (see [Type-State Validator](#type-state-validator)). + +`register_get_handler` is called by the middleware whenever a consumer calls `get_{name}()`. It is also required in the producer `init()` chain and tracked by the type-state validator (see [Type-State Validator](#type-state-validator)). + +#### `FieldGetCaller` and `FieldSetCaller` + +These are `Runtime` associated types bounded by `MethodCaller`: + +```rust +type FieldGetCaller: MethodCaller<(), T, Self>; +type FieldSetCaller: MethodCaller<(T,), T, Self>; +``` + +They are distinct associated types (not aliases) so runtimes can route to FFI-level getter/setter endpoints. Both reuse the full `MethodCaller` call infrastructure: async futures. + +The `interface!` macro generates on the consumer: +- `{name}_get: R::FieldGetCaller` + an async `get_{name}()` convenience wrapper for `WithGetter` fields. +- `{name}_set: R::FieldSetCaller` + an async `set_{name}(val)` convenience wrapper for `WithSetter` fields. + +### Zero-Copy Publish Traits + +These traits form the zero-copy field publish pipeline on the producer side. + +#### `FieldSampleMut` + +Extends `SampleMut` (which provides `DerefMut`) : + +```rust +pub trait FieldSampleMut: concept::SampleMut +where + T: CommData + Debug, +{ + fn update(self) -> Result<()>; +} +``` + +`update()` consumes the sample and commits the written value to the field. It mirrors `EventSampleMut::send()` in the event design. + +#### `SampleMaybeUninit` + +A single uninitialised field slot. The producer writes a value into it, obtaining a `FieldSampleMut` that can be committed via `update()`. + +```rust +pub trait SampleMaybeUninit { + type SampleMut: FieldSampleMut; + + fn write(self, val: T) -> Self::SampleMut; + + /// # Safety + /// The caller must ensure the memory has been properly initialized. + unsafe fn assume_init(self) -> Self::SampleMut; +} +``` +--- + +## Capability Tags + +Each field must declare at least one capability tag. Tags are combined with `+`: + +```rust +left_tire: Field, +exhaust: Field, +``` + +| Tag | Consumer-side generated code | Producer-side impact | +|-----|------------------------------|----------------------| +| `WithGetter` | `{name}_get: R::FieldGetCaller` + `get_{name}() -> impl Future<...>` | `register_get_handler_{name}()` **required** in `init()` chain (type-state guarded: `HandlerNotSet` - `HandlerSet`) | +| `WithSetter` | `{name}_set: R::FieldSetCaller` + `set_{name}(val) -> impl Future<...>` | `register_set_handler_{name}()` **required** in `init()` chain (type-state guarded: `HandlerNotSet` - `HandlerSet`) | +| `WithNotifier` | `{name}: R::FieldSubscriber` (subscribe via `.subscribe(n)`) | Field value changes via `update()` notify all active subscribers | + +Any combination and any ordering of tags is supported. + +--- + +## Get and Set Call Paths + +Both get and set reuse the `MethodCaller` copy path. There is no zero-copy path for get/set on the consumer side (only the producer publish path has zero-copy via `allocate()`). + +**Get path** - read the current field value from the producer: + +```rust +// get_{name}() calls MethodCaller::invoke_with_copy(&self.{name}_get, ()) +match consumer.get_left_tire().await { + Ok(sample) => { + let tire: &Tire = &*sample; // Deref to access Tire + println!("Current pressure: {:?}", tire); + } + Err(e) => eprintln!("Error: {:?}", e), +} +``` + +**Set path** - write a new value to the field on the producer, returns the confirmed value: + +```rust +// set_{name}(val) calls MethodCaller::invoke_with_copy(&self.{name}_set, (val,)) +match consumer.set_left_tire(Tire { pressure: 35.0 }).await { + Ok(sample) => println!("Confirmed pressure: {:?}", *sample), + Err(e) => eprintln!("Error: {:?}", e), +} +``` + +The compiler selects the correct `MethodCaller` specialisation from the `Runtime` associated types (`FieldGetCaller` vs `FieldSetCaller`) - no runtime branching. + +--- + +## Interface Macro Integration + +The `interface!` macro accepts fields using `name: Field` syntax: + +```rust +interface!( + interface VehicleField { + Id = "VehicleFieldInterface", + left_tire: Field, + exhaust: Field, + } +); +``` + +For each field the macro generates: + +**On `VehicleFieldConsumer`**: +- `left_tire: R::FieldSubscriber` - subscribe to change notifications (`WithNotifier`) +- `left_tire_get: R::FieldGetCaller` - underlying get caller (`WithGetter`) +- `left_tire_set: R::FieldSetCaller` - underlying set caller (`WithSetter`) +- `get_left_tire() -> impl Future<...>` - async get convenience wrapper (`WithGetter`) +- `set_left_tire(val) -> impl Future<...>` - async set convenience wrapper (`WithSetter`) + +**On `VehicleFieldValidator`** (returned by `producer.init()`): +- `update_left_tire(value: T) -> Result` — sets the initial field value; advances the type-state from `Uninit` to `Init`. **Generated for every field.** +- `register_set_handler_left_tire(fn)` — registers the set callback; advances `HandlerNotSet` - `HandlerSet`. **Generated only for `WithSetter` fields.** +- `register_get_handler_left_tire(fn)` — registers the get callback; advances `HandlerNotSet` - `HandlerSet`. **Generated only for `WithGetter` fields.** + +The `interface_producer_mixed!` macro emits `#[field_setter_list(left_tire, exhaust)]` and `#[field_getter_list(left_tire, exhaust)]` struct-level attributes on the producer struct so the `TypeStateValidator` proc-macro knows which steps to generate. + +**On `VehicleFieldOfferedProducer`**: +- `left_tire: R::FieldPublisher` - live publisher used to call `update()` after the service is offered. + +--- + +## Type-State Validator + +The `type_state_validator` proc-macro generates a compile-time state machine on the producer initialisation path. It inspects the producer struct and the `#[field_setter_list(...)]` / `#[field_getter_list(...)]` attributes emitted by `interface_producer_mixed!` to know which fields have which capability tags. For each `FieldPublisher` field, up to three independent states are tracked: + +- **Initial value** (`Si`): transitions from `Uninit` to `Init` when `update_{name}()` is called — **all fields, always**. +- **Set handler** (`Hj`): transitions from `HandlerNotSet` to `HandlerSet` when `register_set_handler_{name}()` is called — **`WithSetter` fields only**. +- **Get handler** (`Gk`): transitions from `HandlerNotSet` to `HandlerSet` when `register_get_handler_{name}()` is called — **`WithGetter` fields only**. + +For methods, `Mp` tracks handler registration as before. + +`offer()` is only available once all `Si = Init`, all `Hj = HandlerSet`, all `Gk = HandlerSet`, and all `Mp = HandlerSet`. Calling `offer()` before completing the chain is a compile error. A field with only `WithNotifier` (no getter, no setter) only requires `update_*` before `offer()`. + +```rust +// Compile error: offer() not available until all update_* and handler steps are done +producer.init() + .update_left_tire(initial_tire)? + // missing: register_set_handler_left_tire (WithSetter) + // missing: register_get_handler_left_tire (WithGetter) + // missing: update_exhaust / register_set_handler_exhaust / register_get_handler_exhaust + .offer() // compile error +``` + +```rust +// Correct: all fields initialised and all tag-gated handlers registered +producer + .init() + .register_set_handler_left_tire(|val: Tire| { + println!("Received tire set: {:?}", val); + }) + .register_get_handler_left_tire(|| Tire { pressure: 32.0 }) + .register_set_handler_exhaust(|val: Exhaust| { + let _ = val; + }) + .register_get_handler_exhaust(|| Exhaust {}) + .update_left_tire(initial_tire_value)? + .update_exhaust(initial_exhaust_value)? + .offer()?; +``` + +Note: For event-only interfaces, `producer.offer()` can be called directly. For fields (and methods), `offer()` must be reached via the `producer.init()` chain, and calling `offer()` directly will panic at runtime. + +--- + +## Producer Side API Usage + +The following example is drawn from [`com-api-example/src/field_producer.rs`](../../example/com-api-example/src/field_producer.rs). + +```rust +use score_com::{Builder, FieldPublisher, InstanceSpecifier, Interface, Producer, Runtime}; +use com_api_gen::{Exhaust, Tire, VehicleFieldInterface}; + +fn create_producer_field( + runtime: &R, + service_id: InstanceSpecifier, + initial_tire_value: Tire, + initial_exhaust_value: Exhaust, +) -> <::Producer as Producer>::OfferedProducer +where + ::FieldPublisher: Send + Sync, + ::FieldPublisher: Send, +{ + let producer = runtime + .producer_builder::(service_id) + .build() + .expect("Failed to build producer instance"); + + producer + .init() + // Register set-handler callbacks - required before offer() for WithSetter fields + .register_set_handler_left_tire(move |val: Tire| { + println!("Received tire pressure update: {:?}", val); + }) + .register_set_handler_exhaust(|val: Exhaust| { + let _ = val; + }) + // Register get-handler callbacks - required before offer() for WithGetter fields + .register_get_handler_left_tire(|| Tire { pressure: 32.0 }) + .register_get_handler_exhaust(|| Exhaust {}) + // Set initial field values - required before offer() for all fields + .update_left_tire(initial_tire_value) + .expect("Failed to update left_tire field") + .update_exhaust(initial_exhaust_value) + .expect("Failed to update exhaust field") + .offer() + .expect("Failed to offer producer instance") +} +``` + +Key points: + +- `producer.init()` returns the generated `Validator` type; each `register_set_handler_*`, `register_get_handler_*`, and `update_*` call advances the type-state. +- All required steps must complete before `offer()` is available. The order of calls is flexible. +- `register_set_handler_*` is only generated for `WithSetter` fields; `register_get_handler_*` is only generated for `WithGetter` fields. Fields with neither tag only require `update_*`. +- After `offer()`, the returned `OfferedProducer` holds the live `FieldPublisher` instances for ongoing `update()` calls. + +To update a field at runtime after offering: + +```rust +fn offered_producer_process(offered_producer: VehicleFieldOfferedProducer) { + // Copy-path update - value is copied into the shared-memory slot by the FFI layer + offered_producer + .left_tire + .update(Tire { pressure: 32.0 }) + .expect("Failed to update left_tire field"); + + // Zero-copy update - allocate a slot, write directly, then commit + let slot = offered_producer.left_tire.allocate() + .expect("Allocation failed"); + let sample_mut = slot.write(Tire { pressure: 32.0 }); + sample_mut.update().expect("Failed to commit zero-copy update"); +} +``` + +--- + +## Consumer Side API Usage + +The following examples are drawn from [`com-api-example/src/field_consumer.rs`](../../example/com-api-example/src/field_consumer.rs). + +### Async get - read the current field value + +```rust +match consumer.get_left_tire().await { + Ok(result) => println!("Current tire pressure: {:?}", *result), // Deref to access Tire + Err(e) => eprintln!("Failed to get tire pressure: {:?}", e), +} +``` + +### Async set - write a new value; receive confirmed value back + +```rust +match consumer.set_left_tire(Tire { pressure: 35.0 }).await { + Ok(result) => println!("Confirmed tire pressure after set: {:?}", *result), + Err(e) => eprintln!("Failed to set tire pressure: {:?}", e), +} +``` + +### Subscribe to field-value-change notifications (`WithNotifier`) + +```rust +// subscribe() takes left_tire by value - get/set callers are separate struct fields +// so they remain usable after this move +let subscription = consumer + .left_tire + .subscribe(3) + .expect("Failed to subscribe to field"); + +// Poll for updates (non-blocking) +let mut sample_buf = SampleContainer::new(3); +match subscription.try_receive(&mut sample_buf, 1) { + Ok(n) if n > 0 => { + while let Some(sample) = sample_buf.pop_front() { + println!("Updated tire pressure: {:?}", *sample); + } + } + _ => println!("No new tire pressure updates available"), +} + +``` + +Note: `subscribe()` consumes the `FieldSubscriber` struct field by value. The `{name}_get` and `{name}_set` callers are separate struct fields on the consumer, so they are not consumed and remain usable in the same async context. + +--- + +## TODOs and Improvements + +### LoLa runtime FFI implementation for fields + +All `LolaFieldSubscriber`, `LolaFieldSubscription`, `LolaFieldPublisher`, `LolaFieldGetCaller`, and `LolaFieldSetCaller` methods in `com-api-runtime-lola` are currently `todo!()` placeholders. The FFI bindings to the underlying C++ middleware for field subscribe/notify, get, set, and update are not yet implemented. This blocks all end-to-end field tests. + +### Zero-copy set path on the consumer side + +The current consumer set path (`set_{name}()`) uses `MethodCaller::invoke_with_copy`, which copies the argument value. If the runtime adds support for `invoke_zero_copy` on `FieldSetCaller`, the consumer could pre-allocate a slot via `{name}_set.allocate()` and pass a `ZeroCopyArgs`-wrapped pointer - identical to the method zero-copy path. diff --git a/score/mw/com/rust/design/field_overview.puml b/score/mw/com/rust/design/field_overview.puml new file mode 100644 index 000000000..2603b118d --- /dev/null +++ b/score/mw/com/rust/design/field_overview.puml @@ -0,0 +1,39 @@ +@startuml + +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' +' +' SPDX-License-Identifier: Apache-2.0 + +skinparam linetype ortho +skinparam backgroundColor #FAFAFA +skinparam defaultFontSize 12 +skinparam ArrowColor #333333 +skinparam packageStyle rectangle +skinparam defaultTextAlignment center + +title Field — High-Level Block Overview + +[User Application\n(Producer / Consumer)] as APP + +[score_com_concept\n(FieldSubscriber, FieldSubscription,\nFieldPublisher, FieldSampleMut,\nFieldGetCaller = MethodCaller<(), T>,\nFieldSetCaller = MethodCaller<(T,), T>,\nWithGetter / WithSetter / WithNotifier tags)] as CONCEPT + +[interface!() macro\n+ type_state_validator\n(Generated consumer wrappers\n& producer Validator with\nupdate / register_set_handler\ntype-state guards)] as MACRO + +[com-api-runtime-lola\n(LolaFieldSubscriber,\nLolaFieldSubscription,\nLolaFieldPublisher,\nLolaFieldGetCaller,\nLolaFieldSetCaller)] as RUNTIME + +[FFI\n(Rust-C++ field\nget / set / update /\nsubscribe bindings)] as FFI + +APP --> MACRO : uses generated\nconsumer / producer API +MACRO --> CONCEPT : generated code\nuses concept traits +RUNTIME ..|> CONCEPT : implements +MACRO --> RUNTIME : dispatches at runtime +RUNTIME --> FFI : bridges via + +@enduml diff --git a/score/mw/com/rust/design/field_overview.svg b/score/mw/com/rust/design/field_overview.svg new file mode 100644 index 000000000..4440a33ef --- /dev/null +++ b/score/mw/com/rust/design/field_overview.svg @@ -0,0 +1 @@ +Field — High-Level Block OverviewUser Application(Producer / Consumer)score_com_concept(FieldSubscriber, FieldSubscription,FieldPublisher, FieldSampleMut,FieldGetCaller = MethodCaller<(), T>,FieldSetCaller = MethodCaller<(T,), T>,WithGetter / WithSetter / WithNotifier tags)interface!() macro+ type_state_validator(Generated consumer wrappers& producer Validator withupdate / register_set_handlertype-state guards)com-api-runtime-lola(LolaFieldSubscriber,LolaFieldSubscription,LolaFieldPublisher,LolaFieldGetCaller,LolaFieldSetCaller)FFI(Rust-C++ fieldget / set / update /subscribe bindings)uses generatedconsumer / producer APIgenerated codeuses concept traitsimplementsdispatches at runtimebridges via \ No newline at end of file diff --git a/score/mw/com/rust/design/field_trait_diagram.puml b/score/mw/com/rust/design/field_trait_diagram.puml new file mode 100644 index 000000000..3d2600423 --- /dev/null +++ b/score/mw/com/rust/design/field_trait_diagram.puml @@ -0,0 +1,175 @@ +@startuml + +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' +' +' SPDX-License-Identifier: Apache-2.0 + +skinparam backgroundColor #FAFAFA +skinparam defaultFontSize 12 +skinparam ArrowColor #333333 +skinparam packageStyle rectangle +skinparam defaultTextAlignment center + +interface Runtime { + type FieldSubscriber + type FieldPublisher + type FieldGetCaller + type FieldSetCaller + type MethodReturnSample + --- + + find_service() + + producer_builder() +} + +interface "FieldSubscriber" as FieldSubscriber { + --- + ' Marker supertrait of Subscriber + ' Adds the constraint: Subscription associated type must be FieldSubscription + ' All subscription APIs (new, subscribe) come from Subscriber + ' Generated by interface! on the consumer struct for WithNotifier fields: + ' {name}: R::FieldSubscriber + ' Consumer calls .subscribe(max_num_samples) to receive field-value-change notifications +} + +interface "Subscriber" as Subscriber { + type Subscription: FieldSubscription + --- + + new(identifier, instance_info) -> Result + + subscribe(self, max_num_samples: usize) -> Result +} + +interface "FieldSubscription" as FieldSubscription { + --- + + get_num_new_samples_available() -> Result + + get_free_sample_count() -> Result + --- + ' Extends Subscription which provides: + ' try_receive, receive, cancellable_receive, to_stream + ' get_num_new_samples_available(): how many fresh samples try_receive would return + ' get_free_sample_count(): remaining capacity before the subscription buffer overflows +} + +interface "Subscription" as Subscription { + type Subscriber: Subscriber + type Sample<'a>: Sample + --- + + unsubscribe(self) -> Self::Subscriber + + try_receive(scratch, max_samples) -> Result + + receive(scratch, new_samples, max_samples) -> impl Future<...> + + cancellable_receive(scratch, new_samples, max_samples, cancel) -> impl Future<...> + + to_stream(scratch, max_samples) -> impl Stream<...> +} + +interface "FieldPublisher" as FieldPublisher { + type SampleMaybeUninit<'a>: SampleMaybeUninit> + --- + + new(identifier, instance_info) -> Result + + allocate() -> Result> + + update(value: T) -> Result<()> + + register_set_handler(callback: impl Fn(T) + Send + 'static) + + register_get_handler(callback: impl Fn() -> T + Send + 'static) + --- + ' update() is the copy-path write; for zero-copy use allocate() then SampleMaybeUninit::write() + ' register_set_handler: invoked by middleware when consumer calls set_{name}() + ' - required on producer init() chain before offer() (type-state: HandlerNotSet -> HandlerSet) + ' register_get_handler: invoked by middleware when consumer calls get_{name}() + ' - optional; default returns the last update()d value +} + +interface "SampleMaybeUninit" as SampleMaybeUninit { + type SampleMut: FieldSampleMut + --- + + write(val: T) -> Self::SampleMut + + assume_init(self) -> Self::SampleMut [unsafe] + --- + ' Zero-copy write path: allocate() returns an uninitialised slot + ' write() initialises the slot and returns a FieldSampleMut + ' Mirrors MethodInArgMaybeUninit in the Method design +} + +interface "FieldSampleMut" as FieldSampleMut { + --- + + update(self) -> Result<()> + --- + ' Extends SampleMut: provides DerefMut + ' update() commits the written value to the field — completes the zero-copy publish + ' Mirrors EventSampleMut::send() in the Event design +} + +interface "SampleMut" as SampleMut { + --- + ' DerefMut + ' Base mutable reference to a shared-memory slot +} + +interface "FieldGetCaller" as FieldGetCaller { + --- + ' Bounded by MethodCaller<(), T, R> + ' Distinct associated type on Runtime so runtimes can route to a specific FFI Getter + ' Consumer struct field generated by interface! for WithGetter fields: + ' {name}_get: R::FieldGetCaller + ' Async convenience wrapper also generated: + ' get_{name}() -> impl Future>> + ' calls invoke_with_copy(()) on the underlying MethodCaller +} + +interface "FieldSetCaller" as FieldSetCaller { + --- + ' Bounded by MethodCaller<(T,), T, R> + ' Distinct associated type on Runtime so runtimes can route to a specific FFI Setter + ' Consumer struct field generated by interface! for WithSetter fields: + ' {name}_set: R::FieldSetCaller + ' Async convenience wrapper also generated: + ' set_{name}(val: T) -> impl Future>> + ' calls invoke_with_copy((val,)) on the underlying MethodCaller +} + +interface "MethodCaller" as MethodCaller { + --- + + new(method_name, instance_info) -> Result + + invoke_with_copy(args: Args) -> impl Future>> + + allocate() -> Result + + invoke_zero_copy(ptrs: Args::PtrTuple) -> impl Future>> + --- + ' FieldGetCaller: Args = (), Return = T + ' FieldSetCaller: Args = (T,), Return = T + ' Both reuse the full Method call infrastructure (copy path + future + MethodReturnSample) +} + +interface "MethodReturnSample" as MethodReturnSample { + --- + ' Deref + ' Returned by get_{name}() and set_{name}() wrapper calls + ' The confirmed field value from the producer is accessed via Deref + ' Same associated type as used by Methods — shared infrastructure + ' Runtime concrete types: LolaMethodReturnSample, MockMethodReturnSample +} + + +Runtime --> FieldSubscriber : defines as\nassociated type +Runtime --> FieldPublisher : defines as\nassociated type +Runtime --> FieldGetCaller : defines as\nassociated type\n(bounded by MethodCaller<(),T>) +Runtime --> FieldSetCaller : defines as\nassociated type\n(bounded by MethodCaller<(T,),T>) +Runtime --> MethodReturnSample : defines as\nassociated type + +FieldSubscriber --|> Subscriber : extends\n(marker supertrait) +FieldSubscription --|> Subscription : extends + +Subscriber --> FieldSubscription : Subscription\nassociated type + +FieldPublisher --> SampleMaybeUninit : allocate()\nproduces +SampleMaybeUninit --> FieldSampleMut : write()\nreturns +FieldSampleMut --|> SampleMut : extends + +FieldGetCaller --|> MethodCaller : bounded by\nMethodCaller<(), T, R> +FieldSetCaller --|> MethodCaller : bounded by\nMethodCaller<(T,), T, R> +MethodCaller --> MethodReturnSample : invoke returns + +@enduml diff --git a/score/mw/com/rust/design/field_trait_diagram.svg b/score/mw/com/rust/design/field_trait_diagram.svg new file mode 100644 index 000000000..b35ff9b97 --- /dev/null +++ b/score/mw/com/rust/design/field_trait_diagram.svg @@ -0,0 +1 @@ +Runtimetype FieldSubscriber<T: CommData>type FieldPublisher<T: CommData>type FieldGetCaller<T: CommData>type FieldSetCaller<T: CommData>type MethodReturnSample<T: CommData>find_service()producer_builder()FieldSubscriberT, RSubscriberT, Rtype Subscription: FieldSubscription<T, R>new(identifier, instance_info) -> Result<Self>subscribe(self, max_num_samples: usize) -> Result<Self::Subscription>FieldSubscriptionT, Rget_num_new_samples_available() -> Result<usize>get_free_sample_count() -> Result<usize>SubscriptionT, Rtype Subscriber: Subscriber<T, R>type Sample<'a>: Sample<T>unsubscribe(self) -> Self::Subscribertry_receive(scratch, max_samples) -> Result<usize>receive(scratch, new_samples, max_samples) -> impl Future<...>cancellable_receive(scratch, new_samples, max_samples, cancel) -> impl Future<...>to_stream(scratch, max_samples) -> impl Stream<...>FieldPublisherT, Rtype SampleMaybeUninit<'a>: SampleMaybeUninit<T, SampleMut: FieldSampleMut<T>>new(identifier, instance_info) -> Result<Self>allocate() -> Result<Self::SampleMaybeUninit<'_>>update(value: T) -> Result<()>register_set_handler(callback: impl Fn(T) + Send + 'static)register_get_handler(callback: impl Fn() -> T + Send + 'static)SampleMaybeUninitTtype SampleMut: FieldSampleMut<T>write(val: T) -> Self::SampleMutassume_init(self) -> Self::SampleMut [unsafe]FieldSampleMutTupdate(self) -> Result<()>SampleMutTFieldGetCallerT, RFieldSetCallerT, RMethodCallerArgs, Return, Rnew(method_name, instance_info) -> Result<Self>invoke_with_copy(args: Args) -> impl Future<Output = Result<R::MethodReturnSample<Return>>>allocate() -> Result<Args::UninitTuple>invoke_zero_copy(ptrs: Args::PtrTuple) -> impl Future<Output = Result<R::MethodReturnSample<Return>>>MethodReturnSampleTdefines asassociated typedefines asassociated typedefines asassociated type(bounded by MethodCaller<(),T>)defines asassociated type(bounded by MethodCaller<(T,),T>)defines asassociated typeextends(marker supertrait)extendsSubscriptionassociated typeallocate()produceswrite()returnsextendsbounded byMethodCaller<(), T, R>bounded byMethodCaller<(T,), T, R>invoke returns \ No newline at end of file diff --git a/score/mw/com/rust/score_com_concept/concept.rs b/score/mw/com/rust/score_com_concept/concept.rs index bec7f264e..bfe46220a 100644 --- a/score/mw/com/rust/score_com_concept/concept.rs +++ b/score/mw/com/rust/score_com_concept/concept.rs @@ -122,7 +122,7 @@ pub trait Runtime { // Note: below GATs are to make runtime implementation simpler for Field Methods, // If at the time of implementation no specific need is found for these GATs, // we can remove them and use MethodCaller instead of a separate field-specific design. - + /// `FieldGetCaller` types for the consumer-side async field get operation. /// Distinct from `MethodCaller<(), T>` so runtimes can route to specific `Getter`. type FieldGetCaller: MethodCaller<(), T, Self>; diff --git a/score/mw/com/rust/score_com_concept/field_concept.rs b/score/mw/com/rust/score_com_concept/field_concept.rs index 52fd7c7f4..deb262c45 100644 --- a/score/mw/com/rust/score_com_concept/field_concept.rs +++ b/score/mw/com/rust/score_com_concept/field_concept.rs @@ -79,17 +79,9 @@ pub trait FieldPublisher { where Self: Sized; - /// Get the allocate sample ptr for the field publisher. + /// Get the allocated sample ptr for the field publisher. fn allocate(&self) -> Result>; - /// Update the value of the field with the provided value. - /// This is not zero-copy API. - /// - /// # Parameters - /// * `value` - The value to update for the field. - /// - /// # Returns - /// Return the result of `Result<()>` which contains the status of the update operation. /// Update the value of the field with the provided value. /// The value is taken by value; the FFI layer handles the necessary copy into the shared /// memory slot internally — the same pattern as `Publisher::send(value: T)` for events. diff --git a/score/mw/com/rust/score_com_concept/method_arities_macros.rs b/score/mw/com/rust/score_com_concept/method_arities_macros.rs index cdb61a906..82fc8f79c 100644 --- a/score/mw/com/rust/score_com_concept/method_arities_macros.rs +++ b/score/mw/com/rust/score_com_concept/method_arities_macros.rs @@ -24,7 +24,7 @@ //! Note: `_gen_method_wrapper!` in `interface_macros.rs` self-generates its argument //! identifiers via a counting recursive macro, so it has no separate limit to keep in //! sync - raising the arity here is the only change needed. -//! (I don't think this many arguments will support by clippy linting, +//! (I don't think clippy linting will support this many arguments, //! so we may need to reduce the limit to 4 to 5 in the future based on project clippy linting rules.) //! //! # Arity 0 special case From 0920265ef0f14a19cedaf7ca8c596b01b2815973 Mon Sep 17 00:00:00 2001 From: bharatgoswami Date: Fri, 31 Jul 2026 15:09:30 +0530 Subject: [PATCH 25/25] Rust::com Update the Interface macro modules --- .../interface_consumer_macros.rs | 42 - .../score_com_concept/interface_macros.rs | 1727 +++++++++++++++++ .../interface_producer_macros.rs | 1644 +--------------- score/mw/com/rust/score_com_concept/lib.rs | 5 +- 4 files changed, 1733 insertions(+), 1685 deletions(-) create mode 100644 score/mw/com/rust/score_com_concept/interface_macros.rs diff --git a/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs b/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs index a1cc4846c..ca313c128 100644 --- a/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs @@ -11,48 +11,6 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/// Type-state marker for uninitialized field value state (compile-time tracking). -/// -/// These marker types are never constructed as values - they only appear as generic -/// type parameters inside `PhantomData<(S, H)>` on the generated `{Id}Validator` struct -/// (see `TypeStateValidator` in `score_com_macros`). The compiler's `dead_code` lint -/// flags unit structs that are never instantiated, so it is suppressed here deliberately. -#[allow(dead_code)] -pub struct Uninit; - -/// Type-state marker for initialized field value state (compile-time tracking). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct Init; - -/// Type-state marker for handler not registered (compile-time tracking). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct HandlerNotSet; - -/// Type-state marker for handler registered (compile-time tracking). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct HandlerSet; - -/// Field capability tag: by adding this on interface macro, consumer can call async `get_*()` on this field. -/// Use in `Field` (or combined: `Field`). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct WithGetter; - -/// Field capability tag: by adding this on interface macro, consumer can call async `set_*()` on this field. -/// Use in `Field` (or combined: `Field`). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct WithSetter; - -/// Field capability tag: by adding this on interface macro, consumer can `subscribe()` to field-value-change notifications. -/// Use in `Field` (or combined: `Field`). -/// See [`Uninit`] for why `dead_code` is suppressed. -#[allow(dead_code)] -pub struct WithNotifier; - /// Macro to implement the Consumer trait for a given interface ID and its events. /// /// Generates the Consumer struct with subscribers for each event. diff --git a/score/mw/com/rust/score_com_concept/interface_macros.rs b/score/mw/com/rust/score_com_concept/interface_macros.rs new file mode 100644 index 000000000..9f057acf6 --- /dev/null +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -0,0 +1,1727 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/// Type-state marker for uninitialized field value state (compile-time tracking). +/// +/// These marker types are never constructed as values - they only appear as generic +/// type parameters inside `PhantomData<(S, H)>` on the generated `{Id}Validator` struct +/// (see `TypeStateValidator` in `score_com_macros`). The compiler's `dead_code` lint +/// flags unit structs that are never instantiated, so it is suppressed here deliberately. +#[allow(dead_code)] +pub struct Uninit; + +/// Type-state marker for initialized field value state (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct Init; + +/// Type-state marker for handler not registered (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct HandlerNotSet; + +/// Type-state marker for handler registered (compile-time tracking). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct HandlerSet; + +/// Field capability tag: by adding this on interface macro, consumer can call async `get_*()` on this field. +/// Use in `Field` (or combined: `Field`). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct WithGetter; + +/// Field capability tag: by adding this on interface macro, consumer can call async `set_*()` on this field. +/// Use in `Field` (or combined: `Field`). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct WithSetter; + +/// Field capability tag: by adding this on interface macro, consumer can `subscribe()` to field-value-change notifications. +/// Use in `Field` (or combined: `Field`). +/// See [`Uninit`] for why `dead_code` is suppressed. +#[allow(dead_code)] +pub struct WithNotifier; + +/// Macro to implement the Consumer trait for a given interface ID and its events. +/// + +/// Main interface macro that generates Consumer, Producer, and OfferedProducer types +/// along with all necessary trait implementations. +/// +/// Supports Event-only interfaces (backward compatible) and mixed interfaces containing +/// any combination of `Event`, `Field`, and `method_name(Args) -> Return` members +/// in the same definition block. +/// +/// Automatically generates unique type names from the identifier of macro invocation. +/// For an interface with identifier `{id}`, it generates: +/// - `{id}Interface` - Struct representing the interface with INTERFACE_ID constant +/// - `{id}Consumer` - Consumer implementation with event subscribers, field subscribers, +/// and method callers +/// - `{id}Producer` - Producer implementation +/// - `{id}OfferedProducer` - Offered producer implementation with event publishers, +/// field publishers, and method handlers +/// - Implements the `Interface`, `Consumer`, `Producer`, and `OfferedProducer` traits +/// for the respective types. +/// - `Interface_ID` is generated by default as the module path + interface name, +/// but can be overridden by providing a custom UID as a second parameter to the macro. +/// +/// # Member types +/// - `name: Event` - event subscriber / publisher pair +/// - `name: Field` - field subscriber / publisher pair (with set-handler callback support) +/// - `name(Args) -> Return` - method caller / handler pair (fn-like syntax) +/// +/// # Parameters +/// - Keywords: `interface` followed by the interface identifier and a block of member definitions. +/// - `$id`: Simple identifier used for type name generation (e.g., Vehicle, Engine) +/// - Members can be any mix of `Event`, `Field`, and `name(Args) -> Return` +/// +/// # Example: Event-only with auto-generated ID +/// ```ignore +/// mod abc { +/// use score_com::interface; +/// interface!( +/// interface Vehicle { +/// left_tire: Event, +/// exhaust: Event, +/// } +/// ); +/// } +/// ``` +/// The generated code will include: +/// - `VehicleInterface` struct with `INTERFACE_ID = "abc::Vehicle"` +/// - `VehicleConsumer` struct that implements `Consumer` trait for subscribing to +/// "left_tire" and "exhaust" events. +/// - `VehicleProducer` struct that implements `Producer` trait for producing +/// "left_tire" and "exhaust" events. +/// - `VehicleOfferedProducer` struct that implements `OfferedProducer` trait for offering +/// "left_tire" and "exhaust" events. +/// - `VehicleConsumer`, `VehicleProducer`, `VehicleOfferedProducer` +/// +/// # Example: Event-only with custom ID +/// ```ignore +/// mod abc { +/// use score_com::interface; +/// interface!( +/// interface Vehicle { +/// Id = "AbcInterface", +/// left_tire: Event, +/// exhaust: Event, +/// } +/// ); +/// } +/// ``` +/// Here `Id` is explicitly set to `"AbcInterface"` instead of the default `"abc::Vehicle"`. +/// The generated code will include: +/// - `VehicleInterface` struct with `INTERFACE_ID = "AbcInterface"` +/// - `VehicleConsumer` struct that implements `Consumer` trait for subscribing +/// to "left_tire" and "exhaust" events. +/// - `VehicleProducer` struct that implements `Producer` trait for producing +/// "left_tire" and "exhaust" events. +/// - `VehicleOfferedProducer` struct that implements `OfferedProducer` trait for offering +/// "left_tire" and "exhaust" events. +/// +/// # Example: Mixed interface (Event + Field + Method) with different field tags +/// ```ignore +/// mod abc { +/// use score_com::interface; +/// interface!( +/// interface Vehicle { +/// Id = "AbcInterface", +/// // Event member +/// left_tire: Event, +/// // Field with all three tags: consumer gets subscriber + get caller + set caller +/// tire_pressure: Field, +/// // Field with only WithGetter: consumer gets get caller only +/// speed: Field, +/// // Field with only WithNotifier: consumer gets subscriber only +/// status: Field, +/// // Method member +/// update_left_tire_pressure(Tire) -> (), +/// // Get method +/// get_tire_pressure() -> Tire, +/// } +/// ); +/// } +/// ``` +/// The generated code will include: +/// - `VehicleInterface` struct with `INTERFACE_ID = "AbcInterface"` +/// - `VehicleConsumer` with: +/// - `left_tire: Subscriber` +/// - `tire_pressure: FieldSubscriber` (from `WithNotifier`), +/// `tire_pressure_get: FieldGetCaller` (from `WithGetter`), +/// `tire_pressure_set: FieldSetCaller` (from `WithSetter`) +/// - `speed_get: FieldGetCaller` (from `WithGetter` only — no subscriber) +/// - `status: FieldSubscriber` (from `WithNotifier` only — no get/set callers) +/// - `update_left_tire_pressure: MethodCaller<(Tire,), Tire>` and a convenience `update_left_tire_pressure(arg0: Tire)` method +/// - `get_tire_pressure: MethodCaller<(), Tire>` and a convenience `get_tire_pressure()` method +/// - `VehicleProducer` derives `TypeStateValidator` and requires the `.init()` chain +/// before `offer()` can be called: +/// ```ignore +/// producer.init() +/// .update_tire_pressure(&initial_value)? +/// .register_set_handler_tire_pressure(|v| { /* handle set */ }) +/// .register_get_handler_speed(|| Speed { value: 0 }) +/// .update_status(&initial_status)? +/// .register_calibrate_handler(|tire: Tire| tire) +/// .offer()?; +/// ``` +/// - `VehicleOfferedProducer` contains `left_tire: Publisher` +/// plus the moved field publishers and the active method handler. +/// +/// If the user calls `producer.offer()` directly (without going through `init()`), it will +/// panic at runtime, since the handlers have not been registered yet. +#[macro_export] +macro_rules! interface { + // Default unique ID based on the module path and interface name + (interface $id:ident { $($event_name:ident : Event<$event_type:ty>),+ $(,)? }) => { + $crate::interface_common!($id); + $crate::interface_consumer!($id, $($event_name, Event<$event_type>),+); + $crate::interface_producer!($id, $($event_name, Event<$event_type>),+); + }; + + // Custom unique Id provided by the user + (interface $id:ident { + Id = $uid:expr, + $($event_name:ident : Event<$event_type:ty>),+ $(,)? + }) => { + $crate::interface_common!($id, $uid); + $crate::interface_consumer!($id, $($event_name, Event<$event_type>),+); + $crate::interface_producer!($id, $($event_name, Event<$event_type>),+); + }; + + // This is for backward compatibility for existing users with comma (,) + (interface $id:ident, { + Id = $uid:expr, + $($event_name:ident : Event<$event_type:ty>),+ $(,)? + }) => { + $crate::interface! { + interface $id { + Id = $uid, + $($event_name : Event<$event_type>),+ + } + } + }; + + // Mixed / unified: custom ID + (interface $id:ident { + Id = $uid:expr, + $($members:tt)* + }) => { + $crate::interface_common!($id, $uid); + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[] + @fi[] + @fi_n[] + @fi_g[] + @fi_s[] + @me[] + $($members)* + ); + }; + + // Mixed / unified: auto-generated ID + (interface $id:ident { $($members:tt)* }) => { + $crate::interface_common!($id); + $crate::_interface_collect_members!( + @id[$id, concat!(module_path!(), "::", stringify!($id))] + @ev[] + @fi[] + @fi_n[] + @fi_g[] + @fi_s[] + @me[] + $($members)* + ); + }; +} + +/// Helper for `_interface_collect_members!`. +/// +/// Iterates over the tag list of a single field, adding the field to the correct per-tag +/// accumulator list. When all tags are consumed, calls back to `_interface_collect_members!` +/// with the updated lists and the remaining interface members. +#[doc(hidden)] +#[macro_export] +macro_rules! _field_split_tags { + // Base: all tags consumed - call back to _interface_collect_members! with updated lists + ( + @ctx[ + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + @rest[$($rest:tt)*] + ] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] + @field[$_name:ident : $_t:ty] + @tags[] + ) => { + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)*] + @fi[$($fi_name : $fi_type ,)*] + @fi_n[$($fin_name : $fin_type ,)*] + @fi_g[$($fig_name : $fig_type ,)*] + @fi_s[$($fis_name : $fis_type ,)*] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + $($rest)* + ); + }; + + // WithNotifier: add field to fi_n list, recurse with remaining tags + ( + @ctx[$($ctx:tt)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fi_g:tt)*] + @fi_s[$($fi_s:tt)*] + @field[$name:ident : $t:ty] + @tags[WithNotifier $(, $rest_tag:ident)*] + ) => { + $crate::_field_split_tags!( + @ctx[$($ctx)*] + @fi_n[$($fin_name : $fin_type ,)* $name : $t ,] + @fi_g[$($fi_g)*] + @fi_s[$($fi_s)*] + @field[$name : $t] + @tags[$($rest_tag),*] + ); + }; + + // WithGetter: add field to fi_g list, recurse with remaining tags + ( + @ctx[$($ctx:tt)*] + @fi_n[$($fi_n:tt)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fi_s:tt)*] + @field[$name:ident : $t:ty] + @tags[WithGetter $(, $rest_tag:ident)*] + ) => { + $crate::_field_split_tags!( + @ctx[$($ctx)*] + @fi_n[$($fi_n)*] + @fi_g[$($fig_name : $fig_type ,)* $name : $t ,] + @fi_s[$($fi_s)*] + @field[$name : $t] + @tags[$($rest_tag),*] + ); + }; + + // WithSetter: add field to fi_s list, recurse with remaining tags + ( + @ctx[$($ctx:tt)*] + @fi_n[$($fi_n:tt)*] + @fi_g[$($fi_g:tt)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] + @field[$name:ident : $t:ty] + @tags[WithSetter $(, $rest_tag:ident)*] + ) => { + $crate::_field_split_tags!( + @ctx[$($ctx)*] + @fi_n[$($fi_n)*] + @fi_g[$($fi_g)*] + @fi_s[$($fis_name : $fis_type ,)* $name : $t ,] + @field[$name : $t] + @tags[$($rest_tag),*] + ); + }; + + // Unrecognized tag + ( + @ctx[$($ctx:tt)*] + @fi_n[$($fi_n:tt)*] + @fi_g[$($fi_g:tt)*] + @fi_s[$($fi_s:tt)*] + @field[$name:ident : $_t:ty] + @tags[$unknown:ident $(, $rest_tag:ident)*] + ) => { + compile_error!(concat!( + "interface!: unrecognized field tag `", + stringify!($unknown), + "` on field `", + stringify!($name), + "`. Supported tags: WithGetter, WithSetter, WithNotifier." + )); + }; +} + +/// Internal recursive-macro helper for `interface!`. +/// +/// Accumulates members into typed lists, then calls the mixed generator macros. +/// Field members MUST carry at least one capability tag: `Field`. +/// `Field` without tags is a compile error. +/// Tags control which consumer-side infrastructure is generated per field: +/// - `WithGetter` - `{name}_get: R::FieldGetCaller` + `get_{name}()` async wrapper +/// - `WithSetter` - `{name}_set: R::FieldSetCaller` + `set_{name}(val)` async wrapper +/// - `WithNotifier` - `{name}: R::FieldSubscriber` (subscribe / notifications) +/// Any combination and any ordering of tags is supported. +/// +/// Fields are split into three flat lists during accumulation: +/// @fi_n - fields with WithNotifier tag (name:type) +/// @fi_g - fields with WithGetter tag (name:type) +/// @fi_s - fields with WithSetter tag (name:type) +/// A field with multiple tags appears in multiple lists. +/// The plain @fi list (name:type only) is still kept for forwarding to interface_producer_mixed!. +#[doc(hidden)] +#[macro_export] +macro_rules! _interface_collect_members { + // Base case: nothing left - emit the mixed consumer and producer. + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $(,)? + ) => { + $crate::interface_consumer_mixed!( + $id, + events[$($ev_name : $ev_type ,)*], + fields_notifier[$($fin_name : $fin_type ,)*], + fields_getter[$($fig_name : $fig_type ,)*], + fields_setter[$($fis_name : $fis_type ,)*], + methods[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + ); + $crate::interface_producer_mixed!( + $id, + events[$($ev_name : $ev_type ,)*], + fields[$($fi_name : $fi_type ,)*], + fields_setter[$($fis_name : $fis_type ,)*], + fields_getter[$($fig_name : $fig_type ,)*], + methods[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + ); + }; + + // Event member: `name : Event ,?` + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $name:ident : Event<$t:ty> + $(, $($rest:tt)*)? + ) => { + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)* $name : $t ,] + @fi[$($fi_name : $fi_type ,)*] + @fi_n[$($fin_name : $fin_type ,)*] + @fi_g[$($fig_name : $fig_type ,)*] + @fi_s[$($fis_name : $fis_type ,)*] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + $($($rest)*)? + ); + }; + + // Field member WITH tags: `name : Field ,?` + // Delegates to _field_split_tags! to distribute the field into the per-tag flat lists. + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $name:ident : Field<$t:ty, $first_tag:ident $(+ $rest_tag:ident)*> + $(, $($rest:tt)*)? + ) => { + $crate::_field_split_tags!( + @ctx[ + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)*] + @fi[$($fi_name : $fi_type ,)* $name : $t ,] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] + @rest[$($($rest)*)?] + ] + @fi_n[$($fin_name : $fin_type ,)*] + @fi_g[$($fig_name : $fig_type ,)*] + @fi_s[$($fis_name : $fis_type ,)*] + @field[$name : $t] + @tags[$first_tag $(, $rest_tag)*] + ); + }; + + // Field member WITHOUT tags: `name : Field ,?` compile error. + ( + @id[$_id:ident, $_uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $name:ident : Field<$_t:ty> + $(, $($rest:tt)*)? + ) => { + compile_error!(concat!( + "interface!: Field member `", + stringify!($name), + "` requires at least one capability tag.\n", + "Supported tags: WithGetter, WithSetter, WithNotifier\n", + "Use the `+` syntax to combine tags, e.g.:\n", + " ", stringify!($name), ": Field\n", + " ", stringify!($name), ": Field\n", + " ", stringify!($name), ": Field\n", + " ", stringify!($name), ": Field\n", + "Tags control which consumer-side infrastructure is generated:\n", + " WithGetter - get_*()\n", + " WithSetter - set_*()\n", + " WithNotifier - subscribe()\n", + )); + }; + + // Method member (fn-like syntax): `name(Arg0, Arg1, ...) -> Ret ,?` + ( + @id[$id:ident, $uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $name:ident ( $($arg_ty:ty),* ) -> $ret:ty + $(, $($rest:tt)*)? + ) => { + $crate::_interface_collect_members!( + @id[$id, $uid] + @ev[$($ev_name : $ev_type ,)*] + @fi[$($fi_name : $fi_type ,)*] + @fi_n[$($fin_name : $fin_type ,)*] + @fi_g[$($fig_name : $fig_type ,)*] + @fi_s[$($fis_name : $fis_type ,)*] + @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)* $name [$($arg_ty),*] -> $ret ,] + $($($rest)*)? + ); + }; + + // Catch-all: unrecognized member - emit a clear compile-time error. + ( + @id[$_id:ident, $_uid:expr] + @ev[$($ev_name:ident : $ev_type:ty ,)*] + @fi[$($fi_name:ident : $fi_type:ty ,)*] + @fi_n[$($fin_name:ident : $fin_type:ty ,)*] + @fi_g[$($fig_name:ident : $fig_type:ty ,)*] + @fi_s[$($fis_name:ident : $fis_type:ty ,)*] + @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] + $($unknown:tt)+ + ) => { + compile_error!(concat!( + "interface!: unrecognized member syntax: `", + stringify!($($unknown)+), + "`.\n", + "Supported member types:\n", + " name: Event - event subscriber / publisher pair\n", + " name: Field - field with capability tags\n", + " name(Arg0, Arg1, ...) -> Ret - method caller / handler pair\n", + "Note: Field without tags is not allowed. Specify at least one of:\n", + " WithGetter, WithSetter, WithNotifier\n", + "Example:\n", + " interface!(interface MyIface {\n", + " my_event: Event,\n", + " my_field: Field,\n", + " my_method(MyData) -> MyData,\n", + " });" + )); + }; +} + +/// Macro to create a unique interface struct and implement the Interface trait for it. +/// +/// Generates the INTERFACE_ID constant and associated Consumer/Producer types. +/// INTERFACE_ID is generated by default as the module path + interface name, +/// but can be overridden by providing a custom UID as a second parameter to the macro. +#[macro_export] +macro_rules! interface_common { + // Default: auto ID = module path + type name + ($id:ident) => { + score_com::paste::paste! { + pub struct [<$id Interface>] {} + impl score_com::Interface for [<$id Interface>] { + const INTERFACE_ID: &'static str = + concat!(module_path!(), "::", stringify!($id)); + type Consumer = [<$id Consumer>]; + type Producer = [<$id Producer>]; + } + } + }; + // Explicit ID override + ($id:ident, $uid:expr) => { + score_com::paste::paste! { + pub struct [<$id Interface>] {} + impl score_com::Interface for [<$id Interface>] { + const INTERFACE_ID: &'static str = $uid; + type Consumer = [<$id Consumer>]; + type Producer = [<$id Producer>]; + } + } + }; +} + +mod tests { + /// ``` + /// mod my_module { + /// use score_com::{interface,CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire: Event, + /// exhaust: Event, + /// } + /// ); + /// } + /// ``` + /// This will generate the following types and trait implementations: + /// - `VehicleInterface` struct with `INTERFACE_ID = "my_module::Vehicle"` + /// - `VehicleConsumer`, `VehicleProducer`, `VehicleOfferedProducer` with appropriate + /// trait implementations for the Vehicle interface. + #[cfg(doctest)] + fn interface_macro_with_auto_id() {} + + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// Id = "CustomVehicleInterface", + /// left_tire: Event, + /// exhaust: Event, + /// } + /// ); + /// } + /// ``` + /// This will generate the following types and trait implementations: + /// - `VehicleInterface` struct with `INTERFACE_ID = "CustomVehicleInterface"` + /// - `VehicleConsumer`, `VehicleProducer`, `VehicleOfferedProducer` with appropriate + /// trait implementations for the Vehicle interface. + #[cfg(doctest)] + fn interface_macro_with_custom_id() {} + + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// + /// interface!( + /// interface Vehicle, { + /// Id = "CustomVehicleInterface", + /// left_tire: Event, + /// exhaust: Event, + /// } + /// ); + /// } + /// ``` + /// This will generate the following types and trait implementations: + /// - `VehicleInterface` struct with `INTERFACE_ID = "CustomVehicleInterface"` + /// - `VehicleConsumer`, `VehicleProducer`, `VehicleOfferedProducer` with appropriate + /// trait implementations for the Vehicle interface. + #[cfg(doctest)] + fn interface_macro_with_custom_id_with_comma_for_backend_compatibility() {} + + /// Mixed interface (Event + Field + Method) with a custom ID. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithGetter, WithSetter, WithNotifier}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// Id = "AbcInterface", + /// left_tire: Event, + /// left_tire_field: Field, + /// left_tire_method(Tire) -> Tire, + /// } + /// ); + /// } + /// ``` + /// Generates `VehicleInterface`, `VehicleConsumer`, `VehicleProducer`, + /// and `VehicleOfferedProducer` where: + /// - `VehicleConsumer` has `left_tire: Subscriber`, + /// `left_tire_field: FieldSubscriber` (from `WithNotifier`), + /// `left_tire_field_get: FieldGetCaller` (from `WithGetter`), + /// `left_tire_field_set: FieldSetCaller` (from `WithSetter`), + /// `left_tire_method: MethodCaller<(Tire,), Tire>`, + /// and a convenience `left_tire_method(arg0: Tire)` method. + /// - `VehicleProducer` derives `TypeStateValidator` and requires the `.init()` chain: + /// `producer.init().update_left_tire_field(&val)?.register_set_handler_left_tire_field(f).register_left_tire_method_handler(h).offer()?` + /// - `VehicleOfferedProducer` has `left_tire: Publisher` (created lazily on offer), + /// `left_tire_field: FieldPublisher`, plus the active method handler. + #[cfg(doctest)] + fn interface_macro_mixed() {} + + /// Field with `WithNotifier` only consumer can subscribe to value-change notifications. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithNotifier}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field: FieldSubscriber`. + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No `_get` or `_set` callers are generated on the consumer side. + /// + /// `WithNotifier`-only fields do not generate a `register_set_handler_*` step: + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, WithNotifier}; + /// #[derive(Debug, Reloc, Clone)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { const ID: &'static str = "Tire"; } + /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); + /// fn _check(p: VehicleProducer) { + /// let _ = p.init().register_set_handler_left_tire_field(|_: Tire| {}); + /// } + /// } + /// ``` + /// + /// `WithNotifier`-only fields do not generate a `register_get_handler_*` step: + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, WithNotifier}; + /// #[derive(Debug, Reloc, Clone)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { const ID: &'static str = "Tire"; } + /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); + /// fn _check(p: VehicleProducer) { + /// let _ = p.init().register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); + /// } + /// } + /// ``` + #[cfg(doctest)] + fn interface_macro_field_with_notifier_only() {} + + /// Field with `WithGetter` only consumer can call async `get_*()`. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithGetter, + /// LolaRuntimeImpl as LolaRuntime}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// + /// // Compile-time check `register_get_handler_*` exists for WithGetter fields. + /// #[allow(dead_code)] + /// fn _check_get_handler(p: VehicleProducer) { + /// let _ = p.init().register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); + /// } + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field_get: FieldGetCaller`. + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No subscriber or `_set` caller is generated on the consumer side. + /// + /// `WithGetter`-only fields do not generate a `register_set_handler_*` step: + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, WithGetter}; + /// #[derive(Debug, Reloc, Clone)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { const ID: &'static str = "Tire"; } + /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); + /// fn _check(p: VehicleProducer) { + /// let _ = p.init().register_set_handler_left_tire_field(|_: Tire| {}); + /// } + /// } + /// ``` + #[cfg(doctest)] + fn interface_macro_field_with_getter_only() {} + + /// Field with `WithSetter` only consumer can call async `set_*()`. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithSetter, + /// LolaRuntimeImpl as LolaRuntime}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// + /// // Compile-time check `register_set_handler_*` exists for WithSetter fields. + /// #[allow(dead_code)] + /// fn _check_set_handler(p: VehicleProducer) { + /// let _ = p.init().register_set_handler_left_tire_field(|_: Tire| {}); + /// } + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field_set: FieldSetCaller`. + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No subscriber or `_get` caller is generated on the consumer side. + /// + /// `WithSetter`-only fields do not generate a `register_get_handler_*` step: + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, WithSetter}; + /// #[derive(Debug, Reloc, Clone)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { const ID: &'static str = "Tire"; } + /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); + /// fn _check(p: VehicleProducer) { + /// let _ = p.init().register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); + /// } + /// } + /// ``` + #[cfg(doctest)] + fn interface_macro_field_with_setter_only() {} + + /// Field with `WithGetter + WithNotifier` consumer can both get and subscribe. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithGetter, WithNotifier}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field: FieldSubscriber` (from `WithNotifier`) + /// and `left_tire_field_get: FieldGetCaller` (from `WithGetter`). + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No `_set` caller is generated. + #[cfg(doctest)] + fn interface_macro_field_with_getter_and_notifier() {} + + /// Field with `WithSetter + WithNotifier` consumer can both set and subscribe. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithSetter, WithNotifier}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field: FieldSubscriber` (from `WithNotifier`) + /// and `left_tire_field_set: FieldSetCaller` (from `WithSetter`). + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No `_get` caller is generated. + #[cfg(doctest)] + fn interface_macro_field_with_setter_and_notifier() {} + + /// Field with `WithGetter + WithSetter` consumer can both get and set, without notifications. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithGetter, WithSetter, + /// LolaRuntimeImpl as LolaRuntime}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// + /// // Compile-time proof: both handler methods exist for WithGetter + WithSetter fields. + /// #[allow(dead_code)] + /// fn _assert_both_handlers(p: VehicleProducer) { + /// let v = p.init().register_set_handler_left_tire_field(|_: Tire| {}); + /// let _ = v.register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); + /// } + /// } + /// ``` + /// Generates: + /// - `VehicleConsumer` has `left_tire_field_get: FieldGetCaller` (from `WithGetter`) + /// and `left_tire_field_set: FieldSetCaller` (from `WithSetter`). + /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. + /// No subscriber is generated (no `WithNotifier`). + #[cfg(doctest)] + fn interface_macro_field_with_getter_and_setter() {} + + /// Multiple fields with different tag combinations on the same interface. + /// + /// ``` + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, + /// FieldPublisher, WithGetter, WithSetter, WithNotifier}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// notify_only_field: Field, + /// get_only_field: Field, + /// set_only_field: Field, + /// get_set_field: Field, + /// get_notify_field: Field, + /// set_notify_field: Field, + /// full_field: Field, + /// } + /// ); + /// } + /// ``` + /// Each field generates only the consumer-side accessors for its declared tags: + /// - `notify_only_field`: subscriber only. + /// - `get_only_field`: `_get` caller only. + /// - `set_only_field`: `_set` caller only. + /// - `get_set_field`: `_get` and `_set` callers, no subscriber. + /// - `get_notify_field`: `_get` caller and subscriber, no `_set`. + /// - `set_notify_field`: `_set` caller and subscriber, no `_get`. + /// - `full_field`: subscriber, `_get` caller, and `_set` caller. + /// All fields get a `FieldPublisher` on the producer side regardless of tags. + #[cfg(doctest)] + fn interface_macro_field_all_tag_combinations() {} + + /// Using an unrecognized field tag is a compile-time error. + /// + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// left_tire_field: Field, + /// } + /// ); + /// } + /// ``` + /// This fails to compile because `WithReadOnly` is not a recognized field tag. + /// Supported tags are: `WithGetter`, `WithSetter`, `WithNotifier`. + #[cfg(doctest)] + fn interface_macro_field_unrecognized_tag() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// + /// interface!( + /// interface Vehicle { + /// Id = "CustomVehicleInterface", + /// left_tire: Method, + /// exhaust: Method, + /// } + /// ); + /// } + /// ``` + /// This will fail to compile because `Method` (old syntax without a return type) is not + /// supported. Use fn-like syntax: `method_name(Args) -> Ret`. + #[cfg(doctest)] + fn interface_macro_with_old_method_syntax() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// interface_common!(Vehicle, "CustomVehicleInterface", { + /// left_tire: Event, + /// }); + /// } + /// ``` + /// This will fail to compile because `interface_common!` does not accept member definitions. + /// Use `interface!` for a complete interface definition. + #[cfg(doctest)] + fn interface_macro_with_Field() {} + + /// ``` + /// mod my_module { + /// use score_com::{interface_common, interface_consumer, interface_producer}; + /// use score_com::{CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// interface_common!(Vehicle); + /// interface_consumer!(Vehicle, left_tire, Event, exhaust, Event); + /// interface_producer!(Vehicle, left_tire, Event, exhaust, Event); + /// } + /// ``` + /// This will generate a `VehicleInterface` struct with an `INTERFACE_ID` constant that is + /// automatically generated as the module path plus + /// the interface name (e.g."my_module::Vehicle"). + /// It will also define associated `Consumer` and `Producer` types for the `VehicleInterface`. + /// The `VehicleConsumer` struct will implement the `Consumer` trait for the + /// `VehicleInterface`, with subscribers for the `left_tire` and `exhaust` events. + /// The `VehicleProducer` struct will implement the `Producer` trait for the + /// `VehicleInterface`, with publishers for the `left_tire` and `exhaust` events. + #[cfg(doctest)] + fn individual_common_macro_with_auto_id() {} + + /// ``` + /// mod my_module { + /// use score_com::{interface_common, interface_consumer, interface_producer}; + /// use score_com::{CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// interface_common!(Vehicle, "CustomVehicleInterface"); + /// interface_consumer!(Vehicle, left_tire, Event, exhaust, Event); + /// interface_producer!(Vehicle, left_tire, Event, exhaust, Event); + /// } + /// ``` + /// This will generate a `VehicleInterface` struct with an `INTERFACE_ID` constant set to + /// "CustomVehicleInterface". + /// It will also define associated `Consumer` and `Producer` types for the `VehicleInterface`. + /// The `VehicleConsumer` struct will implement the `Consumer` trait for the + /// `VehicleInterface`, with subscribers for the `left_tire` and `exhaust` events. + /// The `VehicleProducer` struct will implement the `Producer` trait for the + /// `VehicleInterface`, with publishers for the `left_tire` and `exhaust` events. + #[cfg(doctest)] + fn individual_macro_with_custom_id() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface_common, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// interface_common!(Vehicle, "CustomVehicleInterface", { + /// left_tire: Event, + /// exhaust: Event, + /// }); + /// } + /// ``` + /// This will fail to compile because the `interface_common!` macro does not accept event + /// definitions and will produce a compile-time error indicating that the macro does not support + /// event definitions. + #[cfg(doctest)] + fn interface_common_macro_with_events() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface_common, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// interface_common!(Vehicle, "CustomVehicleInterface", { + /// left_tire: Method, + /// exhaust: Method, + /// }); + /// } + /// ``` + /// This will fail to compile because the `interface_common!` macro does not accept method + /// definitions and will produce a compile-time error indicating that the macro does not support + /// method definitions. + #[cfg(doctest)] + fn interface_common_macro_with_methods() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface_common, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// interface_common!(Vehicle, "CustomVehicleInterface", { + /// left_tire: Field, + /// exhaust: Field, + /// }); + /// } + /// ``` + /// This will fail to compile because the `interface_common!` macro does not accept field + /// definitions and will produce a compile-time error indicating + /// that the macro does not support field definitions. + #[cfg(doctest)] + fn interface_common_macro_with_fields() {} + + /// ``` + /// mod my_module { + /// use score_com::{interface_consumer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// interface_consumer!(Vehicle, left_tire, Event, exhaust, Event); + /// } + /// ``` + /// This will generate a `VehicleConsumer` struct that implements the `Consumer` trait for + /// the `VehicleInterface`, with subscribers for the `left_tire` and `exhaust` events. + /// The generated `VehicleConsumer` struct will have fields for each event subscriber, and + /// the `new` method will initialize these subscribers using + /// the runtime's `Subscriber::new` method. + /// The macro will also include error handling to ensure that subscriber creation failures are + /// properly reported. + #[cfg(doctest)] + fn interface_consumer_macro() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface_consumer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// interface_consumer!(Vehicle, left_tire, Method, exhaust, Method); + /// } + /// ``` + /// This will fail to compile because the `interface_consumer!` macro does not support Method + /// definitions and will produce a compile-time error indicating that + /// Method definitions are not supported. + #[cfg(doctest)] + fn interface_consumer_macro_with_Method() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface_consumer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// interface_consumer!(Vehicle, left_tire, Field, exhaust, Field); + /// } + /// ``` + /// This will fail to compile because the `interface_consumer!` macro does not support Field + /// definitions and will produce a compile-time error indicating that + /// Field definitions are not supported. + #[cfg(doctest)] + fn interface_consumer_macro_with_Field() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface_producer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// interface_producer!(Vehicle, left_tire, Event, exhaust, Event); + /// } + /// ``` + /// This will generate a `VehicleProducer` struct that implements the `Producer` trait for + /// the `VehicleInterface`, with publishers for the `left_tire` and `exhaust` events. + /// So it requires interface_common macro to be called before to generate + /// the VehicleInterface struct + /// and implement the Interface trait for it. + #[cfg(doctest)] + fn interface_producer_macro() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface_producer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// interface_producer!(Vehicle, left_tire, Method, exhaust, Method); + /// } + /// ``` + /// This will fail to compile because the `interface_producer!` macro does not support Method + /// definitions and will produce a compile-time error indicating that + /// Method definitions are not supported. + #[cfg(doctest)] + fn interface_producer_macro_with_Method() {} + + /// ```compile_fail + /// mod my_module { + /// use score_com::{interface_producer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; + /// } + /// + /// #[derive(Debug, Reloc)] + /// #[repr(C)] + /// pub struct Exhaust {} + /// impl CommData for Exhaust { + /// const ID: &'static str = "Exhaust"; + /// } + /// interface_producer!(Vehicle, left_tire, Field, exhaust, Field); + /// } + /// ``` + /// This will fail to compile because the `interface_producer!` macro does not support Field + /// definitions and will produce a compile-time error indicating that + /// Field definitions are not supported. + #[cfg(doctest)] + fn interface_producer_macro_with_Field() {} +} + +#[cfg(test)] +#[allow(dead_code)] +#[allow(unused_imports)] +mod validation_tests { + #[test] + fn test_interface_id_auto_generated() { + mod test_module { + use score_com::{CommData, ProviderInfo, Publisher, Reloc, Subscriber}; + + #[derive(Debug, Reloc)] + #[repr(C)] + pub struct Tire { + pub pressure: f32, + } + impl CommData for Tire { + const ID: &'static str = "Tire"; + } + + score_com::interface!( + interface Vehicle { + left_tire: Event, + } + ); + + pub fn validate() { + // Referencing VehicleInterface by name is itself proof the type was generated; + // the compiler enforces the name at compile time. + let interface_id = ::INTERFACE_ID; + let expected_id = concat!(module_path!(), "::", "Vehicle"); + assert_eq!( + interface_id, expected_id, + "Interface ID mismatch for VehicleInterface" + ); + } + } + test_module::validate(); + } + + #[test] + fn test_consumer_type_generated() { + mod test_module { + use score_com::{ + CommData, Consumer, LolaRuntimeImpl as LolaRuntime, ProviderInfo, Publisher, Reloc, + Subscriber, + }; + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Tire { + pub pressure: f32, + } + impl CommData for Tire { + const ID: &'static str = "Tire"; + } + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Exhaust { + pub temp: f32, + } + impl CommData for Exhaust { + const ID: &'static str = "Exhaust"; + } + + score_com::interface!( + interface Vehicle { + left_tire: Event, + exhaust: Event, + } + ); + + pub fn validate() { + // Referencing VehicleConsumer by name is proof the type was generated. + // The size check verifies that subscriber fields were generated. + assert!( + std::mem::size_of::>() > 0, + "VehicleConsumer should have subscriber fields" + ); + } + } + test_module::validate(); + } + + #[test] + fn test_producer_type_generated() { + mod test_module { + use score_com::{ + CommData, LolaRuntimeImpl as LolaRuntime, Producer, ProviderInfo, Publisher, Reloc, + Subscriber, + }; + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Tire { + pub pressure: f32, + } + impl CommData for Tire { + const ID: &'static str = "Tire"; + } + + score_com::interface!( + interface Engine { + rpm: Event, + } + ); + + pub fn validate() { + // Referencing EngineProducer by name is proof the type was generated. + let _ = core::marker::PhantomData::>; + } + } + test_module::validate(); + } + + #[test] + fn test_offered_producer_type_generated() { + mod test_module { + use score_com::{ + CommData, LolaRuntimeImpl as LolaRuntime, Producer, ProviderInfo, Publisher, Reloc, + Subscriber, + }; + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Tire { + pub pressure: f32, + } + impl CommData for Tire { + const ID: &'static str = "Tire"; + } + + score_com::interface!( + interface Transmission { + gear: Event, + } + ); + + pub fn validate() { + // Referencing TransmissionOfferedProducer by name is proof the type + // was generated. + // The size check verifies that publisher fields were generated. + assert!( + std::mem::size_of::>() > 0, + "TransmissionOfferedProducer should have publisher fields" + ); + } + } + test_module::validate(); + } + + #[test] + fn test_interface_with_custom_id_validation() { + mod test_module { + use score_com::{CommData, Interface, ProviderInfo, Publisher, Reloc, Subscriber}; + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Tire { + pub pressure: f32, + } + impl CommData for Tire { + const ID: &'static str = "Tire"; + } + + score_com::interface!( + interface Battery, { + Id = "com.example.Battery", + voltage: Event, + } + ); + + pub fn validate() { + // Referencing BatteryInterface by name is proof the type was generated with the + // correct naming convention; the custom ID is a meaningful runtime assertion. + let interface_id = ::INTERFACE_ID; + assert_eq!( + interface_id, "com.example.Battery", + "Custom interface ID should match provided UID" + ); + } + } + test_module::validate(); + } + + #[test] + fn test_interface_with_multiple_events_validation() { + mod test_module { + use score_com::{ + CommData, Interface, LolaRuntimeImpl as LolaRuntime, ProviderInfo, Publisher, + Reloc, Subscriber, + }; + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Event1Data { + pub value: i32, + } + impl CommData for Event1Data { + const ID: &'static str = "Event1Data"; + } + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Event2Data { + pub value: f64, + } + impl CommData for Event2Data { + const ID: &'static str = "Event2Data"; + } + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Event3Data { + pub value: bool, + } + impl CommData for Event3Data { + const ID: &'static str = "Event3Data"; + } + + score_com::interface!( + interface MultiEvent { + event_one: Event, + event_two: Event, + event_three: Event, + } + ); + + pub fn validate() { + // ID assertion is meaningful at runtime. + let interface_id = ::INTERFACE_ID; + assert_eq!( + interface_id, + concat!(module_path!(), "::", "MultiEvent"), + "Interface ID should be auto-generated from module path and interface name" + ); + + // Referencing Consumer, Producer, and OfferedProducer by name proves all four + // types were generated, the compiler enforces the names at compile time. + let _ = core::marker::PhantomData::>; + let _ = core::marker::PhantomData::>; + let _ = core::marker::PhantomData::>; + } + } + test_module::validate(); + } + + #[test] + fn test_interface_type_consistency_across_traits() { + mod test_module { + use score_com::{ + CommData, Interface, LolaRuntimeImpl as LolaRuntime, ProviderInfo, Publisher, + Reloc, Subscriber, + }; + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Tire { + pub pressure: f32, + } + impl CommData for Tire { + const ID: &'static str = "Tire"; + } + + score_com::interface!( + interface Suspension { + travel: Event, + } + ); + + pub fn validate() { + // Referencing all four generated types by their expected names is proof of + // consistent naming, the compiler enforces the names at compile time. + let _ = core::marker::PhantomData::; + let _ = core::marker::PhantomData::>; + let _ = core::marker::PhantomData::>; + let _ = core::marker::PhantomData::>; + + let interface_id = ::INTERFACE_ID; + assert_eq!( + interface_id, + concat!(module_path!(), "::", "Suspension"), + "Interface ID should be auto-generated from module path and interface name" + ); + } + } + test_module::validate(); + } + + #[test] + fn test_interface_naming_convention_validation() { + mod test_module { + use score_com::{ + CommData, LolaRuntimeImpl as LolaRuntime, ProviderInfo, Publisher, Reloc, + Subscriber, + }; + + #[derive(Debug, Reloc, Clone)] + #[repr(C)] + pub struct Data { + pub value: u32, + } + impl CommData for Data { + const ID: &'static str = "Data"; + } + + score_com::interface!( + interface ABS { + status: Event, + } + ); + + pub fn validate() { + // Referencing each generated type by its expected name is itself the proof of + // correct naming conventions, the compiler enforces the names at compile time. + // Pattern: {Name}Interface, {Name}Consumer, {Name}Producer, {Name}OfferedProducer + let _ = core::marker::PhantomData::; + let _ = core::marker::PhantomData::>; + let _ = core::marker::PhantomData::>; + let _ = core::marker::PhantomData::>; + } + } + test_module::validate(); + } +} diff --git a/score/mw/com/rust/score_com_concept/interface_producer_macros.rs b/score/mw/com/rust/score_com_concept/interface_producer_macros.rs index c9b3f2ef4..090e3a40a 100644 --- a/score/mw/com/rust/score_com_concept/interface_producer_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_producer_macros.rs @@ -11,498 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/// Main interface macro that generates Consumer, Producer, and OfferedProducer types -/// along with all necessary trait implementations. -/// -/// Supports Event-only interfaces (backward compatible) and mixed interfaces containing -/// any combination of `Event`, `Field`, and `method_name(Args) -> Return` members -/// in the same definition block. -/// -/// Automatically generates unique type names from the identifier of macro invocation. -/// For an interface with identifier `{id}`, it generates: -/// - `{id}Interface` - Struct representing the interface with INTERFACE_ID constant -/// - `{id}Consumer` - Consumer implementation with event subscribers, field subscribers, -/// and method callers -/// - `{id}Producer` - Producer implementation -/// - `{id}OfferedProducer` - Offered producer implementation with event publishers, -/// field publishers, and method handlers -/// - Implements the `Interface`, `Consumer`, `Producer`, and `OfferedProducer` traits -/// for the respective types. -/// - `Interface_ID` is generated by default as the module path + interface name, -/// but can be overridden by providing a custom UID as a second parameter to the macro. -/// -/// # Member types -/// - `name: Event` - event subscriber / publisher pair -/// - `name: Field` - field subscriber / publisher pair (with set-handler callback support) -/// - `name(Args) -> Return` - method caller / handler pair (fn-like syntax) -/// -/// # Parameters -/// - Keywords: `interface` followed by the interface identifier and a block of member definitions. -/// - `$id`: Simple identifier used for type name generation (e.g., Vehicle, Engine) -/// - Members can be any mix of `Event`, `Field`, and `name(Args) -> Return` -/// -/// # Example: Event-only with auto-generated ID -/// ```ignore -/// mod abc { -/// use score_com::interface; -/// interface!( -/// interface Vehicle { -/// left_tire: Event, -/// exhaust: Event, -/// } -/// ); -/// } -/// ``` -/// The generated code will include: -/// - `VehicleInterface` struct with `INTERFACE_ID = "abc::Vehicle"` -/// - `VehicleConsumer` struct that implements `Consumer` trait for subscribing to -/// "left_tire" and "exhaust" events. -/// - `VehicleProducer` struct that implements `Producer` trait for producing -/// "left_tire" and "exhaust" events. -/// - `VehicleOfferedProducer` struct that implements `OfferedProducer` trait for offering -/// "left_tire" and "exhaust" events. -/// - `VehicleConsumer`, `VehicleProducer`, `VehicleOfferedProducer` -/// -/// # Example: Mixed interface (Event + Field + Method) with custom ID -/// ```ignore -/// mod abc { -/// use score_com::interface; -/// interface!( -/// interface Vehicle { -/// Id = "AbcInterface", -/// left_tire: Event, -/// left_tire_field: Field, -/// left_tire_method(Tire) -> Tire, -/// } -/// ); -/// } -/// ``` -/// Here Id is explicitly set to "AbcInterface" instead of the default "abc::Vehicle". -/// The generated code will include: -/// - `VehicleInterface` struct with `INTERFACE_ID = "AbcInterface"` -/// - `VehicleConsumer` with `left_tire: Subscriber`, `left_tire_field: FieldSubscriber`, -/// `left_tire_method: MethodCaller<(Tire,), Tire>` and a convenience `left_tire_method(arg0: Tire)` method. -/// - `VehicleProducer` (derives `TypeStateValidator`) with `left_tire_field: FieldPublisher`, -/// `left_tire_method: MethodHandler<(Tire,), Tire>`. Requires `.init()` chain before `.offer()`. -/// - `VehicleOfferedProducer` with event publisher `left_tire`, plus moved field publisher and -/// method handler. -/// - For `left_tire_field`, the user needs to both update the initial value and register the -/// set-handler callback, using the same `init()` chain, before offering the producer instance. -/// -/// The code will look like this: -/// ```ignore -/// let producer = producer_builder.build().expect("Failed to build producer instance"); -/// producer.init() -/// .update_left_tire_field(&initial_value)? -/// .register_set_handler_left_tire_field(|value| { -/// println!("Received left_tire_field update: {:?}", value); -/// }) -/// .register_left_tire_method_handler(|tire: Tire| { -/// println!("Received left_tire_method call with tire: {:?}", tire); -/// tire -/// }) -/// .offer()?; -/// ``` -/// In the code above, if the user forgets to register the field set-handler or the method -/// handler, it will be a compile-time error, since `init()` requires all handlers to be -/// registered before `offer()` becomes available. -/// -/// If the user calls `producer.offer()` directly (without going through `init()`), it will -/// panic at runtime, since the handlers have not been registered yet. -#[macro_export] -macro_rules! interface { - // Default unique ID based on the module path and interface name - (interface $id:ident { $($event_name:ident : Event<$event_type:ty>),+ $(,)? }) => { - $crate::interface_common!($id); - $crate::interface_consumer!($id, $($event_name, Event<$event_type>),+); - $crate::interface_producer!($id, $($event_name, Event<$event_type>),+); - }; - - // Custom unique Id provided by the user - (interface $id:ident { - Id = $uid:expr, - $($event_name:ident : Event<$event_type:ty>),+ $(,)? - }) => { - $crate::interface_common!($id, $uid); - $crate::interface_consumer!($id, $($event_name, Event<$event_type>),+); - $crate::interface_producer!($id, $($event_name, Event<$event_type>),+); - }; - - // This is for backward compatibility for existing users with comma (,) - (interface $id:ident, { - Id = $uid:expr, - $($event_name:ident : Event<$event_type:ty>),+ $(,)? - }) => { - $crate::interface! { - interface $id { - Id = $uid, - $($event_name : Event<$event_type>),+ - } - } - }; - - // Mixed / unified: custom ID - (interface $id:ident { - Id = $uid:expr, - $($members:tt)* - }) => { - $crate::interface_common!($id, $uid); - $crate::_interface_collect_members!( - @id[$id, $uid] - @ev[] - @fi[] - @fi_n[] - @fi_g[] - @fi_s[] - @me[] - $($members)* - ); - }; - - // Mixed / unified: auto-generated ID - (interface $id:ident { $($members:tt)* }) => { - $crate::interface_common!($id); - $crate::_interface_collect_members!( - @id[$id, concat!(module_path!(), "::", stringify!($id))] - @ev[] - @fi[] - @fi_n[] - @fi_g[] - @fi_s[] - @me[] - $($members)* - ); - }; -} - -/// Helper for `_interface_collect_members!`. -/// -/// Iterates over the tag list of a single field, adding the field to the correct per-tag -/// accumulator list. When all tags are consumed, calls back to `_interface_collect_members!` -/// with the updated lists and the remaining interface members. -#[doc(hidden)] -#[macro_export] -macro_rules! _field_split_tags { - // Base: all tags consumed - call back to _interface_collect_members! with updated lists - ( - @ctx[ - @id[$id:ident, $uid:expr] - @ev[$($ev_name:ident : $ev_type:ty ,)*] - @fi[$($fi_name:ident : $fi_type:ty ,)*] - @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] - @rest[$($rest:tt)*] - ] - @fi_n[$($fin_name:ident : $fin_type:ty ,)*] - @fi_g[$($fig_name:ident : $fig_type:ty ,)*] - @fi_s[$($fis_name:ident : $fis_type:ty ,)*] - @field[$_name:ident : $_t:ty] - @tags[] - ) => { - $crate::_interface_collect_members!( - @id[$id, $uid] - @ev[$($ev_name : $ev_type ,)*] - @fi[$($fi_name : $fi_type ,)*] - @fi_n[$($fin_name : $fin_type ,)*] - @fi_g[$($fig_name : $fig_type ,)*] - @fi_s[$($fis_name : $fis_type ,)*] - @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] - $($rest)* - ); - }; - - // WithNotifier: add field to fi_n list, recurse with remaining tags - ( - @ctx[$($ctx:tt)*] - @fi_n[$($fin_name:ident : $fin_type:ty ,)*] - @fi_g[$($fi_g:tt)*] - @fi_s[$($fi_s:tt)*] - @field[$name:ident : $t:ty] - @tags[WithNotifier $(, $rest_tag:ident)*] - ) => { - $crate::_field_split_tags!( - @ctx[$($ctx)*] - @fi_n[$($fin_name : $fin_type ,)* $name : $t ,] - @fi_g[$($fi_g)*] - @fi_s[$($fi_s)*] - @field[$name : $t] - @tags[$($rest_tag),*] - ); - }; - - // WithGetter: add field to fi_g list, recurse with remaining tags - ( - @ctx[$($ctx:tt)*] - @fi_n[$($fi_n:tt)*] - @fi_g[$($fig_name:ident : $fig_type:ty ,)*] - @fi_s[$($fi_s:tt)*] - @field[$name:ident : $t:ty] - @tags[WithGetter $(, $rest_tag:ident)*] - ) => { - $crate::_field_split_tags!( - @ctx[$($ctx)*] - @fi_n[$($fi_n)*] - @fi_g[$($fig_name : $fig_type ,)* $name : $t ,] - @fi_s[$($fi_s)*] - @field[$name : $t] - @tags[$($rest_tag),*] - ); - }; - - // WithSetter: add field to fi_s list, recurse with remaining tags - ( - @ctx[$($ctx:tt)*] - @fi_n[$($fi_n:tt)*] - @fi_g[$($fi_g:tt)*] - @fi_s[$($fis_name:ident : $fis_type:ty ,)*] - @field[$name:ident : $t:ty] - @tags[WithSetter $(, $rest_tag:ident)*] - ) => { - $crate::_field_split_tags!( - @ctx[$($ctx)*] - @fi_n[$($fi_n)*] - @fi_g[$($fi_g)*] - @fi_s[$($fis_name : $fis_type ,)* $name : $t ,] - @field[$name : $t] - @tags[$($rest_tag),*] - ); - }; - - // Unrecognized tag - ( - @ctx[$($ctx:tt)*] - @fi_n[$($fi_n:tt)*] - @fi_g[$($fi_g:tt)*] - @fi_s[$($fi_s:tt)*] - @field[$name:ident : $_t:ty] - @tags[$unknown:ident $(, $rest_tag:ident)*] - ) => { - compile_error!(concat!( - "interface!: unrecognized field tag `", - stringify!($unknown), - "` on field `", - stringify!($name), - "`. Supported tags: WithGetter, WithSetter, WithNotifier." - )); - }; -} - -/// Internal recursive-macro helper for `interface!`. -/// -/// Accumulates members into typed lists, then calls the mixed generator macros. -/// Field members MUST carry at least one capability tag: `Field`. -/// `Field` without tags is a compile error. -/// Tags control which consumer-side infrastructure is generated per field: -/// - `WithGetter` - `{name}_get: R::FieldGetCaller` + `get_{name}()` async wrapper -/// - `WithSetter` - `{name}_set: R::FieldSetCaller` + `set_{name}(val)` async wrapper -/// - `WithNotifier` - `{name}: R::FieldSubscriber` (subscribe / notifications) -/// Any combination and any ordering of tags is supported. -/// -/// Fields are split into three flat lists during accumulation: -/// @fi_n - fields with WithNotifier tag (name:type) -/// @fi_g - fields with WithGetter tag (name:type) -/// @fi_s - fields with WithSetter tag (name:type) -/// A field with multiple tags appears in multiple lists. -/// The plain @fi list (name:type only) is still kept for forwarding to interface_producer_mixed!. -#[doc(hidden)] -#[macro_export] -macro_rules! _interface_collect_members { - // Base case: nothing left - emit the mixed consumer and producer. - ( - @id[$id:ident, $uid:expr] - @ev[$($ev_name:ident : $ev_type:ty ,)*] - @fi[$($fi_name:ident : $fi_type:ty ,)*] - @fi_n[$($fin_name:ident : $fin_type:ty ,)*] - @fi_g[$($fig_name:ident : $fig_type:ty ,)*] - @fi_s[$($fis_name:ident : $fis_type:ty ,)*] - @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] - $(,)? - ) => { - $crate::interface_consumer_mixed!( - $id, - events[$($ev_name : $ev_type ,)*], - fields_notifier[$($fin_name : $fin_type ,)*], - fields_getter[$($fig_name : $fig_type ,)*], - fields_setter[$($fis_name : $fis_type ,)*], - methods[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] - ); - $crate::interface_producer_mixed!( - $id, - events[$($ev_name : $ev_type ,)*], - fields[$($fi_name : $fi_type ,)*], - fields_setter[$($fis_name : $fis_type ,)*], - fields_getter[$($fig_name : $fig_type ,)*], - methods[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] - ); - }; - - // Event member: `name : Event ,?` - ( - @id[$id:ident, $uid:expr] - @ev[$($ev_name:ident : $ev_type:ty ,)*] - @fi[$($fi_name:ident : $fi_type:ty ,)*] - @fi_n[$($fin_name:ident : $fin_type:ty ,)*] - @fi_g[$($fig_name:ident : $fig_type:ty ,)*] - @fi_s[$($fis_name:ident : $fis_type:ty ,)*] - @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] - $name:ident : Event<$t:ty> - $(, $($rest:tt)*)? - ) => { - $crate::_interface_collect_members!( - @id[$id, $uid] - @ev[$($ev_name : $ev_type ,)* $name : $t ,] - @fi[$($fi_name : $fi_type ,)*] - @fi_n[$($fin_name : $fin_type ,)*] - @fi_g[$($fig_name : $fig_type ,)*] - @fi_s[$($fis_name : $fis_type ,)*] - @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] - $($($rest)*)? - ); - }; - - // Field member WITH tags: `name : Field ,?` - // Delegates to _field_split_tags! to distribute the field into the per-tag flat lists. - ( - @id[$id:ident, $uid:expr] - @ev[$($ev_name:ident : $ev_type:ty ,)*] - @fi[$($fi_name:ident : $fi_type:ty ,)*] - @fi_n[$($fin_name:ident : $fin_type:ty ,)*] - @fi_g[$($fig_name:ident : $fig_type:ty ,)*] - @fi_s[$($fis_name:ident : $fis_type:ty ,)*] - @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] - $name:ident : Field<$t:ty, $first_tag:ident $(+ $rest_tag:ident)*> - $(, $($rest:tt)*)? - ) => { - $crate::_field_split_tags!( - @ctx[ - @id[$id, $uid] - @ev[$($ev_name : $ev_type ,)*] - @fi[$($fi_name : $fi_type ,)* $name : $t ,] - @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)*] - @rest[$($($rest)*)?] - ] - @fi_n[$($fin_name : $fin_type ,)*] - @fi_g[$($fig_name : $fig_type ,)*] - @fi_s[$($fis_name : $fis_type ,)*] - @field[$name : $t] - @tags[$first_tag $(, $rest_tag)*] - ); - }; - - // Field member WITHOUT tags: `name : Field ,?` compile error. - ( - @id[$_id:ident, $_uid:expr] - @ev[$($ev_name:ident : $ev_type:ty ,)*] - @fi[$($fi_name:ident : $fi_type:ty ,)*] - @fi_n[$($fin_name:ident : $fin_type:ty ,)*] - @fi_g[$($fig_name:ident : $fig_type:ty ,)*] - @fi_s[$($fis_name:ident : $fis_type:ty ,)*] - @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] - $name:ident : Field<$_t:ty> - $(, $($rest:tt)*)? - ) => { - compile_error!(concat!( - "interface!: Field member `", - stringify!($name), - "` requires at least one capability tag.\n", - "Supported tags: WithGetter, WithSetter, WithNotifier\n", - "Use the `+` syntax to combine tags, e.g.:\n", - " ", stringify!($name), ": Field\n", - " ", stringify!($name), ": Field\n", - " ", stringify!($name), ": Field\n", - " ", stringify!($name), ": Field\n", - "Tags control which consumer-side infrastructure is generated:\n", - " WithGetter - get_*()\n", - " WithSetter - set_*()\n", - " WithNotifier - subscribe()\n", - )); - }; - - // Method member (fn-like syntax): `name(Arg0, Arg1, ...) -> Ret ,?` - ( - @id[$id:ident, $uid:expr] - @ev[$($ev_name:ident : $ev_type:ty ,)*] - @fi[$($fi_name:ident : $fi_type:ty ,)*] - @fi_n[$($fin_name:ident : $fin_type:ty ,)*] - @fi_g[$($fig_name:ident : $fig_type:ty ,)*] - @fi_s[$($fis_name:ident : $fis_type:ty ,)*] - @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] - $name:ident ( $($arg_ty:ty),* ) -> $ret:ty - $(, $($rest:tt)*)? - ) => { - $crate::_interface_collect_members!( - @id[$id, $uid] - @ev[$($ev_name : $ev_type ,)*] - @fi[$($fi_name : $fi_type ,)*] - @fi_n[$($fin_name : $fin_type ,)*] - @fi_g[$($fig_name : $fig_type ,)*] - @fi_s[$($fis_name : $fis_type ,)*] - @me[$($me_name [$($me_arg_ty),*] -> $me_ret ,)* $name [$($arg_ty),*] -> $ret ,] - $($($rest)*)? - ); - }; - - // Catch-all: unrecognized member - emit a clear compile-time error. - ( - @id[$_id:ident, $_uid:expr] - @ev[$($ev_name:ident : $ev_type:ty ,)*] - @fi[$($fi_name:ident : $fi_type:ty ,)*] - @fi_n[$($fin_name:ident : $fin_type:ty ,)*] - @fi_g[$($fig_name:ident : $fig_type:ty ,)*] - @fi_s[$($fis_name:ident : $fis_type:ty ,)*] - @me[$($me_name:ident [$($me_arg_ty:ty),*] -> $me_ret:ty ,)*] - $($unknown:tt)+ - ) => { - compile_error!(concat!( - "interface!: unrecognized member syntax: `", - stringify!($($unknown)+), - "`.\n", - "Supported member types:\n", - " name: Event - event subscriber / publisher pair\n", - " name: Field - field with capability tags\n", - " name(Arg0, Arg1, ...) -> Ret - method caller / handler pair\n", - "Note: Field without tags is not allowed. Specify at least one of:\n", - " WithGetter, WithSetter, WithNotifier\n", - "Example:\n", - " interface!(interface MyIface {\n", - " my_event: Event,\n", - " my_field: Field,\n", - " my_method(MyData) -> MyData,\n", - " });" - )); - }; -} - -/// Macro to create a unique interface struct and implement the Interface trait for it. -/// -/// Generates the INTERFACE_ID constant and associated Consumer/Producer types. -/// INTERFACE_ID is generated by default as the module path + interface name, -/// but can be overridden by providing a custom UID as a second parameter to the macro. -#[macro_export] -macro_rules! interface_common { - // Default: auto ID = module path + type name - ($id:ident) => { - score_com::paste::paste! { - pub struct [<$id Interface>] {} - impl score_com::Interface for [<$id Interface>] { - const INTERFACE_ID: &'static str = - concat!(module_path!(), "::", stringify!($id)); - type Consumer = [<$id Consumer>]; - type Producer = [<$id Producer>]; - } - } - }; - // Explicit ID override - ($id:ident, $uid:expr) => { - score_com::paste::paste! { - pub struct [<$id Interface>] {} - impl score_com::Interface for [<$id Interface>] { - const INTERFACE_ID: &'static str = $uid; - type Consumer = [<$id Consumer>]; - type Producer = [<$id Producer>]; - } - } - }; -} +// Root interface!, interface_common!, _field_split_tags!, _interface_collect_members!, +// tag structs, and all tests have been moved to interface_macros.rs. /// This is Event specific. /// Macro to implement the Producer and OfferedProducer traits for @@ -743,1153 +253,3 @@ macro_rules! interface_producer_mixed { } }; } - -mod tests { - /// ``` - /// mod my_module { - /// use score_com::{interface,CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// left_tire: Event, - /// exhaust: Event, - /// } - /// ); - /// } - /// ``` - /// This will generate the following types and trait implementations: - /// - `VehicleInterface` struct with `INTERFACE_ID = "my_module::Vehicle"` - /// - `VehicleConsumer`, `VehicleProducer`, `VehicleOfferedProducer` with appropriate - /// trait implementations for the Vehicle interface. - #[cfg(doctest)] - fn interface_macro_with_auto_id() {} - - /// ``` - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// Id = "CustomVehicleInterface", - /// left_tire: Event, - /// exhaust: Event, - /// } - /// ); - /// } - /// ``` - /// This will generate the following types and trait implementations: - /// - `VehicleInterface` struct with `INTERFACE_ID = "CustomVehicleInterface"` - /// - `VehicleConsumer`, `VehicleProducer`, `VehicleOfferedProducer` with appropriate - /// trait implementations for the Vehicle interface. - #[cfg(doctest)] - fn interface_macro_with_custom_id() {} - - /// ``` - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// - /// interface!( - /// interface Vehicle, { - /// Id = "CustomVehicleInterface", - /// left_tire: Event, - /// exhaust: Event, - /// } - /// ); - /// } - /// ``` - /// This will generate the following types and trait implementations: - /// - `VehicleInterface` struct with `INTERFACE_ID = "CustomVehicleInterface"` - /// - `VehicleConsumer`, `VehicleProducer`, `VehicleOfferedProducer` with appropriate - /// trait implementations for the Vehicle interface. - #[cfg(doctest)] - fn interface_macro_with_custom_id_with_comma_for_backend_compatibility() {} - - /// Mixed interface (Event + Field + Method) with a custom ID. - /// - /// ``` - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// FieldPublisher, WithGetter, WithSetter, WithNotifier}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// Id = "AbcInterface", - /// left_tire: Event, - /// left_tire_field: Field, - /// left_tire_method(Tire) -> Tire, - /// } - /// ); - /// } - /// ``` - /// Generates `VehicleInterface`, `VehicleConsumer`, `VehicleProducer`, - /// and `VehicleOfferedProducer` where: - /// - `VehicleConsumer` has `left_tire: Subscriber`, - /// `left_tire_field: FieldSubscriber` (from `WithNotifier`), - /// `left_tire_field_get: FieldGetCaller` (from `WithGetter`), - /// `left_tire_field_set: FieldSetCaller` (from `WithSetter`), - /// `left_tire_method: MethodCaller<(Tire,), Tire>`, - /// and a convenience `left_tire_method(arg0: Tire)` method. - /// - `VehicleProducer` derives `TypeStateValidator` and requires the `.init()` chain: - /// `producer.init().update_left_tire_field(&val)?.register_set_handler_left_tire_field(f).register_left_tire_method_handler(h).offer()?` - /// - `VehicleOfferedProducer` has `left_tire: Publisher` (created lazily on offer), - /// `left_tire_field: FieldPublisher`, plus the active method handler. - #[cfg(doctest)] - fn interface_macro_mixed() {} - - /// Field with `WithNotifier` only consumer can subscribe to value-change notifications. - /// - /// ``` - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// FieldPublisher, WithNotifier}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// left_tire_field: Field, - /// } - /// ); - /// } - /// ``` - /// Generates: - /// - `VehicleConsumer` has `left_tire_field: FieldSubscriber`. - /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. - /// No `_get` or `_set` callers are generated on the consumer side. - /// - /// `WithNotifier`-only fields do not generate a `register_set_handler_*` step: - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, WithNotifier}; - /// #[derive(Debug, Reloc, Clone)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { const ID: &'static str = "Tire"; } - /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); - /// fn _check(p: VehicleProducer) { - /// let _ = p.init().register_set_handler_left_tire_field(|_: Tire| {}); - /// } - /// } - /// ``` - /// - /// `WithNotifier`-only fields do not generate a `register_get_handler_*` step: - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, WithNotifier}; - /// #[derive(Debug, Reloc, Clone)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { const ID: &'static str = "Tire"; } - /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); - /// fn _check(p: VehicleProducer) { - /// let _ = p.init().register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); - /// } - /// } - /// ``` - #[cfg(doctest)] - fn interface_macro_field_with_notifier_only() {} - - /// Field with `WithGetter` only consumer can call async `get_*()`. - /// - /// ``` - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// FieldPublisher, WithGetter, - /// LolaRuntimeImpl as LolaRuntime}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// left_tire_field: Field, - /// } - /// ); - /// - /// // Compile-time check `register_get_handler_*` exists for WithGetter fields. - /// #[allow(dead_code)] - /// fn _check_get_handler(p: VehicleProducer) { - /// let _ = p.init().register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); - /// } - /// } - /// ``` - /// Generates: - /// - `VehicleConsumer` has `left_tire_field_get: FieldGetCaller`. - /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. - /// No subscriber or `_set` caller is generated on the consumer side. - /// - /// `WithGetter`-only fields do not generate a `register_set_handler_*` step: - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, WithGetter}; - /// #[derive(Debug, Reloc, Clone)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { const ID: &'static str = "Tire"; } - /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); - /// fn _check(p: VehicleProducer) { - /// let _ = p.init().register_set_handler_left_tire_field(|_: Tire| {}); - /// } - /// } - /// ``` - #[cfg(doctest)] - fn interface_macro_field_with_getter_only() {} - - /// Field with `WithSetter` only consumer can call async `set_*()`. - /// - /// ``` - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// FieldPublisher, WithSetter, - /// LolaRuntimeImpl as LolaRuntime}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// left_tire_field: Field, - /// } - /// ); - /// - /// // Compile-time check `register_set_handler_*` exists for WithSetter fields. - /// #[allow(dead_code)] - /// fn _check_set_handler(p: VehicleProducer) { - /// let _ = p.init().register_set_handler_left_tire_field(|_: Tire| {}); - /// } - /// } - /// ``` - /// Generates: - /// - `VehicleConsumer` has `left_tire_field_set: FieldSetCaller`. - /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. - /// No subscriber or `_get` caller is generated on the consumer side. - /// - /// `WithSetter`-only fields do not generate a `register_get_handler_*` step: - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, WithSetter}; - /// #[derive(Debug, Reloc, Clone)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { const ID: &'static str = "Tire"; } - /// score_com::interface!(interface Vehicle { left_tire_field: Field, }); - /// fn _check(p: VehicleProducer) { - /// let _ = p.init().register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); - /// } - /// } - /// ``` - #[cfg(doctest)] - fn interface_macro_field_with_setter_only() {} - - /// Field with `WithGetter + WithNotifier` consumer can both get and subscribe. - /// - /// ``` - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// FieldPublisher, WithGetter, WithNotifier}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// left_tire_field: Field, - /// } - /// ); - /// } - /// ``` - /// Generates: - /// - `VehicleConsumer` has `left_tire_field: FieldSubscriber` (from `WithNotifier`) - /// and `left_tire_field_get: FieldGetCaller` (from `WithGetter`). - /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. - /// No `_set` caller is generated. - #[cfg(doctest)] - fn interface_macro_field_with_getter_and_notifier() {} - - /// Field with `WithSetter + WithNotifier` consumer can both set and subscribe. - /// - /// ``` - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// FieldPublisher, WithSetter, WithNotifier}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// left_tire_field: Field, - /// } - /// ); - /// } - /// ``` - /// Generates: - /// - `VehicleConsumer` has `left_tire_field: FieldSubscriber` (from `WithNotifier`) - /// and `left_tire_field_set: FieldSetCaller` (from `WithSetter`). - /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. - /// No `_get` caller is generated. - #[cfg(doctest)] - fn interface_macro_field_with_setter_and_notifier() {} - - /// Field with `WithGetter + WithSetter` consumer can both get and set, without notifications. - /// - /// ``` - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// FieldPublisher, WithGetter, WithSetter, - /// LolaRuntimeImpl as LolaRuntime}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// left_tire_field: Field, - /// } - /// ); - /// - /// // Compile-time proof: both handler methods exist for WithGetter + WithSetter fields. - /// #[allow(dead_code)] - /// fn _assert_both_handlers(p: VehicleProducer) { - /// let v = p.init().register_set_handler_left_tire_field(|_: Tire| {}); - /// let _ = v.register_get_handler_left_tire_field(|| Tire { pressure: 0.0 }); - /// } - /// } - /// ``` - /// Generates: - /// - `VehicleConsumer` has `left_tire_field_get: FieldGetCaller` (from `WithGetter`) - /// and `left_tire_field_set: FieldSetCaller` (from `WithSetter`). - /// - `VehicleOfferedProducer` has `left_tire_field: FieldPublisher`. - /// No subscriber is generated (no `WithNotifier`). - #[cfg(doctest)] - fn interface_macro_field_with_getter_and_setter() {} - - /// Multiple fields with different tag combinations on the same interface. - /// - /// ``` - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher, - /// FieldPublisher, WithGetter, WithSetter, WithNotifier}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// notify_only_field: Field, - /// get_only_field: Field, - /// set_only_field: Field, - /// get_set_field: Field, - /// get_notify_field: Field, - /// set_notify_field: Field, - /// full_field: Field, - /// } - /// ); - /// } - /// ``` - /// Each field generates only the consumer-side accessors for its declared tags: - /// - `notify_only_field`: subscriber only. - /// - `get_only_field`: `_get` caller only. - /// - `set_only_field`: `_set` caller only. - /// - `get_set_field`: `_get` and `_set` callers, no subscriber. - /// - `get_notify_field`: `_get` caller and subscriber, no `_set`. - /// - `set_notify_field`: `_set` caller and subscriber, no `_get`. - /// - `full_field`: subscriber, `_get` caller, and `_set` caller. - /// All fields get a `FieldPublisher` on the producer side regardless of tags. - #[cfg(doctest)] - fn interface_macro_field_all_tag_combinations() {} - - /// Using an unrecognized field tag is a compile-time error. - /// - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// left_tire_field: Field, - /// } - /// ); - /// } - /// ``` - /// This fails to compile because `WithReadOnly` is not a recognized field tag. - /// Supported tags are: `WithGetter`, `WithSetter`, `WithNotifier`. - #[cfg(doctest)] - fn interface_macro_field_unrecognized_tag() {} - - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// - /// interface!( - /// interface Vehicle { - /// Id = "CustomVehicleInterface", - /// left_tire: Method, - /// exhaust: Method, - /// } - /// ); - /// } - /// ``` - /// This will fail to compile because `Method` (old syntax without a return type) is not - /// supported. Use fn-like syntax: `method_name(Args) -> Ret`. - #[cfg(doctest)] - fn interface_macro_with_old_method_syntax() {} - - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// interface_common!(Vehicle, "CustomVehicleInterface", { - /// left_tire: Event, - /// }); - /// } - /// ``` - /// This will fail to compile because `interface_common!` does not accept member definitions. - /// Use `interface!` for a complete interface definition. - #[cfg(doctest)] - fn interface_macro_with_Field() {} - - /// ``` - /// mod my_module { - /// use score_com::{interface_common, interface_consumer, interface_producer}; - /// use score_com::{CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// interface_common!(Vehicle); - /// interface_consumer!(Vehicle, left_tire, Event, exhaust, Event); - /// interface_producer!(Vehicle, left_tire, Event, exhaust, Event); - /// } - /// ``` - /// This will generate a `VehicleInterface` struct with an `INTERFACE_ID` constant that is - /// automatically generated as the module path plus - /// the interface name (e.g."my_module::Vehicle"). - /// It will also define associated `Consumer` and `Producer` types for the `VehicleInterface`. - /// The `VehicleConsumer` struct will implement the `Consumer` trait for the - /// `VehicleInterface`, with subscribers for the `left_tire` and `exhaust` events. - /// The `VehicleProducer` struct will implement the `Producer` trait for the - /// `VehicleInterface`, with publishers for the `left_tire` and `exhaust` events. - #[cfg(doctest)] - fn individual_common_macro_with_auto_id() {} - - /// ``` - /// mod my_module { - /// use score_com::{interface_common, interface_consumer, interface_producer}; - /// use score_com::{CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// interface_common!(Vehicle, "CustomVehicleInterface"); - /// interface_consumer!(Vehicle, left_tire, Event, exhaust, Event); - /// interface_producer!(Vehicle, left_tire, Event, exhaust, Event); - /// } - /// ``` - /// This will generate a `VehicleInterface` struct with an `INTERFACE_ID` constant set to - /// "CustomVehicleInterface". - /// It will also define associated `Consumer` and `Producer` types for the `VehicleInterface`. - /// The `VehicleConsumer` struct will implement the `Consumer` trait for the - /// `VehicleInterface`, with subscribers for the `left_tire` and `exhaust` events. - /// The `VehicleProducer` struct will implement the `Producer` trait for the - /// `VehicleInterface`, with publishers for the `left_tire` and `exhaust` events. - #[cfg(doctest)] - fn individual_macro_with_custom_id() {} - - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface_common, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// interface_common!(Vehicle, "CustomVehicleInterface", { - /// left_tire: Event, - /// exhaust: Event, - /// }); - /// } - /// ``` - /// This will fail to compile because the `interface_common!` macro does not accept event - /// definitions and will produce a compile-time error indicating that the macro does not support - /// event definitions. - #[cfg(doctest)] - fn interface_common_macro_with_events() {} - - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface_common, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// interface_common!(Vehicle, "CustomVehicleInterface", { - /// left_tire: Method, - /// exhaust: Method, - /// }); - /// } - /// ``` - /// This will fail to compile because the `interface_common!` macro does not accept method - /// definitions and will produce a compile-time error indicating that the macro does not support - /// method definitions. - #[cfg(doctest)] - fn interface_common_macro_with_methods() {} - - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface_common, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// interface_common!(Vehicle, "CustomVehicleInterface", { - /// left_tire: Field, - /// exhaust: Field, - /// }); - /// } - /// ``` - /// This will fail to compile because the `interface_common!` macro does not accept field - /// definitions and will produce a compile-time error indicating - /// that the macro does not support field definitions. - #[cfg(doctest)] - fn interface_common_macro_with_fields() {} - - /// ``` - /// mod my_module { - /// use score_com::{interface_consumer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// interface_consumer!(Vehicle, left_tire, Event, exhaust, Event); - /// } - /// ``` - /// This will generate a `VehicleConsumer` struct that implements the `Consumer` trait for - /// the `VehicleInterface`, with subscribers for the `left_tire` and `exhaust` events. - /// The generated `VehicleConsumer` struct will have fields for each event subscriber, and - /// the `new` method will initialize these subscribers using - /// the runtime's `Subscriber::new` method. - /// The macro will also include error handling to ensure that subscriber creation failures are - /// properly reported. - #[cfg(doctest)] - fn interface_consumer_macro() {} - - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface_consumer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// interface_consumer!(Vehicle, left_tire, Method, exhaust, Method); - /// } - /// ``` - /// This will fail to compile because the `interface_consumer!` macro does not support Method - /// definitions and will produce a compile-time error indicating that - /// Method definitions are not supported. - #[cfg(doctest)] - fn interface_consumer_macro_with_Method() {} - - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface_consumer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// interface_consumer!(Vehicle, left_tire, Field, exhaust, Field); - /// } - /// ``` - /// This will fail to compile because the `interface_consumer!` macro does not support Field - /// definitions and will produce a compile-time error indicating that - /// Field definitions are not supported. - #[cfg(doctest)] - fn interface_consumer_macro_with_Field() {} - - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface_producer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// interface_producer!(Vehicle, left_tire, Event, exhaust, Event); - /// } - /// ``` - /// This will generate a `VehicleProducer` struct that implements the `Producer` trait for - /// the `VehicleInterface`, with publishers for the `left_tire` and `exhaust` events. - /// So it requires interface_common macro to be called before to generate - /// the VehicleInterface struct - /// and implement the Interface trait for it. - #[cfg(doctest)] - fn interface_producer_macro() {} - - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface_producer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// interface_producer!(Vehicle, left_tire, Method, exhaust, Method); - /// } - /// ``` - /// This will fail to compile because the `interface_producer!` macro does not support Method - /// definitions and will produce a compile-time error indicating that - /// Method definitions are not supported. - #[cfg(doctest)] - fn interface_producer_macro_with_Method() {} - - /// ```compile_fail - /// mod my_module { - /// use score_com::{interface_producer, CommData, Reloc, ProviderInfo, Subscriber, Publisher}; - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Tire { pub pressure: f32 } - /// impl CommData for Tire { - /// const ID: &'static str = "Tire"; - /// } - /// - /// #[derive(Debug, Reloc)] - /// #[repr(C)] - /// pub struct Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; - /// } - /// interface_producer!(Vehicle, left_tire, Field, exhaust, Field); - /// } - /// ``` - /// This will fail to compile because the `interface_producer!` macro does not support Field - /// definitions and will produce a compile-time error indicating that - /// Field definitions are not supported. - #[cfg(doctest)] - fn interface_producer_macro_with_Field() {} -} - -#[cfg(test)] -#[allow(dead_code)] -#[allow(unused_imports)] -mod validation_tests { - #[test] - fn test_interface_id_auto_generated() { - mod test_module { - use score_com::{CommData, ProviderInfo, Publisher, Reloc, Subscriber}; - - #[derive(Debug, Reloc)] - #[repr(C)] - pub struct Tire { - pub pressure: f32, - } - impl CommData for Tire { - const ID: &'static str = "Tire"; - } - - score_com::interface!( - interface Vehicle { - left_tire: Event, - } - ); - - pub fn validate() { - // Referencing VehicleInterface by name is itself proof the type was generated; - // the compiler enforces the name at compile time. - let interface_id = ::INTERFACE_ID; - let expected_id = concat!(module_path!(), "::", "Vehicle"); - assert_eq!( - interface_id, expected_id, - "Interface ID mismatch for VehicleInterface" - ); - } - } - test_module::validate(); - } - - #[test] - fn test_consumer_type_generated() { - mod test_module { - use score_com::{ - CommData, Consumer, LolaRuntimeImpl as LolaRuntime, ProviderInfo, Publisher, Reloc, - Subscriber, - }; - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Tire { - pub pressure: f32, - } - impl CommData for Tire { - const ID: &'static str = "Tire"; - } - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Exhaust { - pub temp: f32, - } - impl CommData for Exhaust { - const ID: &'static str = "Exhaust"; - } - - score_com::interface!( - interface Vehicle { - left_tire: Event, - exhaust: Event, - } - ); - - pub fn validate() { - // Referencing VehicleConsumer by name is proof the type was generated. - // The size check verifies that subscriber fields were generated. - assert!( - std::mem::size_of::>() > 0, - "VehicleConsumer should have subscriber fields" - ); - } - } - test_module::validate(); - } - - #[test] - fn test_producer_type_generated() { - mod test_module { - use score_com::{ - CommData, LolaRuntimeImpl as LolaRuntime, Producer, ProviderInfo, Publisher, Reloc, - Subscriber, - }; - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Tire { - pub pressure: f32, - } - impl CommData for Tire { - const ID: &'static str = "Tire"; - } - - score_com::interface!( - interface Engine { - rpm: Event, - } - ); - - pub fn validate() { - // Referencing EngineProducer by name is proof the type was generated. - let _ = core::marker::PhantomData::>; - } - } - test_module::validate(); - } - - #[test] - fn test_offered_producer_type_generated() { - mod test_module { - use score_com::{ - CommData, LolaRuntimeImpl as LolaRuntime, Producer, ProviderInfo, Publisher, Reloc, - Subscriber, - }; - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Tire { - pub pressure: f32, - } - impl CommData for Tire { - const ID: &'static str = "Tire"; - } - - score_com::interface!( - interface Transmission { - gear: Event, - } - ); - - pub fn validate() { - // Referencing TransmissionOfferedProducer by name is proof the type - // was generated. - // The size check verifies that publisher fields were generated. - assert!( - std::mem::size_of::>() > 0, - "TransmissionOfferedProducer should have publisher fields" - ); - } - } - test_module::validate(); - } - - #[test] - fn test_interface_with_custom_id_validation() { - mod test_module { - use score_com::{CommData, Interface, ProviderInfo, Publisher, Reloc, Subscriber}; - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Tire { - pub pressure: f32, - } - impl CommData for Tire { - const ID: &'static str = "Tire"; - } - - score_com::interface!( - interface Battery, { - Id = "com.example.Battery", - voltage: Event, - } - ); - - pub fn validate() { - // Referencing BatteryInterface by name is proof the type was generated with the - // correct naming convention; the custom ID is a meaningful runtime assertion. - let interface_id = ::INTERFACE_ID; - assert_eq!( - interface_id, "com.example.Battery", - "Custom interface ID should match provided UID" - ); - } - } - test_module::validate(); - } - - #[test] - fn test_interface_with_multiple_events_validation() { - mod test_module { - use score_com::{ - CommData, Interface, LolaRuntimeImpl as LolaRuntime, ProviderInfo, Publisher, - Reloc, Subscriber, - }; - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Event1Data { - pub value: i32, - } - impl CommData for Event1Data { - const ID: &'static str = "Event1Data"; - } - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Event2Data { - pub value: f64, - } - impl CommData for Event2Data { - const ID: &'static str = "Event2Data"; - } - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Event3Data { - pub value: bool, - } - impl CommData for Event3Data { - const ID: &'static str = "Event3Data"; - } - - score_com::interface!( - interface MultiEvent { - event_one: Event, - event_two: Event, - event_three: Event, - } - ); - - pub fn validate() { - // ID assertion is meaningful at runtime. - let interface_id = ::INTERFACE_ID; - assert_eq!( - interface_id, - concat!(module_path!(), "::", "MultiEvent"), - "Interface ID should be auto-generated from module path and interface name" - ); - - // Referencing Consumer, Producer, and OfferedProducer by name proves all four - // types were generated, the compiler enforces the names at compile time. - let _ = core::marker::PhantomData::>; - let _ = core::marker::PhantomData::>; - let _ = core::marker::PhantomData::>; - } - } - test_module::validate(); - } - - #[test] - fn test_interface_type_consistency_across_traits() { - mod test_module { - use score_com::{ - CommData, Interface, LolaRuntimeImpl as LolaRuntime, ProviderInfo, Publisher, - Reloc, Subscriber, - }; - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Tire { - pub pressure: f32, - } - impl CommData for Tire { - const ID: &'static str = "Tire"; - } - - score_com::interface!( - interface Suspension { - travel: Event, - } - ); - - pub fn validate() { - // Referencing all four generated types by their expected names is proof of - // consistent naming, the compiler enforces the names at compile time. - let _ = core::marker::PhantomData::; - let _ = core::marker::PhantomData::>; - let _ = core::marker::PhantomData::>; - let _ = core::marker::PhantomData::>; - - let interface_id = ::INTERFACE_ID; - assert_eq!( - interface_id, - concat!(module_path!(), "::", "Suspension"), - "Interface ID should be auto-generated from module path and interface name" - ); - } - } - test_module::validate(); - } - - #[test] - fn test_interface_naming_convention_validation() { - mod test_module { - use score_com::{ - CommData, LolaRuntimeImpl as LolaRuntime, ProviderInfo, Publisher, Reloc, - Subscriber, - }; - - #[derive(Debug, Reloc, Clone)] - #[repr(C)] - pub struct Data { - pub value: u32, - } - impl CommData for Data { - const ID: &'static str = "Data"; - } - - score_com::interface!( - interface ABS { - status: Event, - } - ); - - pub fn validate() { - // Referencing each generated type by its expected name is itself the proof of - // correct naming conventions, the compiler enforces the names at compile time. - // Pattern: {Name}Interface, {Name}Consumer, {Name}Producer, {Name}OfferedProducer - let _ = core::marker::PhantomData::; - let _ = core::marker::PhantomData::>; - let _ = core::marker::PhantomData::>; - let _ = core::marker::PhantomData::>; - } - } - test_module::validate(); - } -} diff --git a/score/mw/com/rust/score_com_concept/lib.rs b/score/mw/com/rust/score_com_concept/lib.rs index 2963b9b54..b3e848512 100644 --- a/score/mw/com/rust/score_com_concept/lib.rs +++ b/score/mw/com/rust/score_com_concept/lib.rs @@ -24,6 +24,7 @@ mod concept; mod error; mod field_concept; mod interface_consumer_macros; +mod interface_macros; mod interface_producer_macros; mod method_arities_macros; mod method_concept; @@ -31,7 +32,9 @@ mod reloc; pub use concept::*; pub use error::*; pub use field_concept::*; -pub use interface_consumer_macros::{HandlerNotSet, HandlerSet, Init, Uninit, WithGetter, WithNotifier, WithSetter}; +pub use interface_macros::{ + HandlerNotSet, HandlerSet, Init, Uninit, WithGetter, WithNotifier, WithSetter, +}; pub use method_concept::*; #[doc(hidden)] pub use paste;