diff --git a/score/mw/com/example/com-api-example/BUILD b/score/mw/com/example/com-api-example/BUILD index 10e025029..ac8ae26d0 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..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 @@ -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,52 @@ 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, ...) -> score_com::Result>. +// 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, + } +); + +// 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. +// 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", + left_tire: Field, + exhaust: Field, + } +); + +// 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, + } +); 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..e423717bb --- /dev/null +++ b/score/mw/com/example/com-api-example/src/field_consumer.rs @@ -0,0 +1,101 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +// 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, Interface, Runtime, SampleContainer, + ServiceDiscovery, Subscriber, Subscription, +}; + +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, +) -> 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") +} + +// 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. +#[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. + match consumer.get_left_tire().await { + Ok(result) => println!("Current tire pressure (async get): {:?}", *result), + Err(e) => eprintln!("Failed to get tire pressure: {:?}", e), + } + + // 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 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"); + + // 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"), + } + + // subscription is automatically unsubscribed when dropped +} 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..dcbb06987 --- /dev/null +++ b/score/mw/com/example/com-api-example/src/field_producer.rs @@ -0,0 +1,96 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +// 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; + +// 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 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, + 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() + // 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.). + // TODO: in working example add that logic to demonstrate the set handler usage. + }) + .register_set_handler_exhaust(|val: Exhaust| { + 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) + .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 +#[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 }; + 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..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,12 +12,18 @@ ********************************************************************************/ pub mod consumer; +// 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; -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..103b5ef2e --- /dev/null +++ b/score/mw/com/example/com-api-example/src/method_consumer.rs @@ -0,0 +1,135 @@ +/******************************************************************************** + * 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 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. + +// 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, Interface, MethodCaller, + MethodInArgMaybeUninit, Runtime, ServiceDiscovery, +}; + +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, +) -> 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 allocated 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 }; + 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), + } + // 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() + .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. +#[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>` + // 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. +// 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 }; + 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..78caf31aa --- /dev/null +++ b/score/mw/com/example/com-api-example/src/method_producer.rs @@ -0,0 +1,73 @@ +/******************************************************************************** + * 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 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, InstanceSpecifier, Interface, Producer, Runtime}; + +use com_api_gen::{Tire, VehicleMethodsInterface}; + +#[allow(dead_code)] +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. +// 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. +#[allow(dead_code)] +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") +} + +#[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/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..aa03bdee1 --- /dev/null +++ b/score/mw/com/example/com-api-example/src/mixed_monitor.rs @@ -0,0 +1,324 @@ +/******************************************************************************** + * 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`) 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). +// +// 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 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**. +#[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. + }) + // 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") + // Field: exhaust_field + .register_set_handler_exhaust_field(|val: Exhaust| { + 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) -> () + .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/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/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/field_consumer.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs new file mode 100644 index 000000000..5f72e1311 --- /dev/null +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_consumer.rs @@ -0,0 +1,117 @@ +/******************************************************************************** + * 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, 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. +/// 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 +{ +} + +/// 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!() + } +} + +/// 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..5ca3f2f75 --- /dev/null +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/field_producer.rs @@ -0,0 +1,113 @@ +/******************************************************************************** + * 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: &'static str, _instance_info: LolaProviderInfo) -> Result { + todo!() + } + fn allocate(&self) -> Result> { + todo!() + } + 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_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-lola/lib.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/lib.rs index 1f9d80df6..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 @@ -26,13 +26,24 @@ //! that utilize the COM API abstractions. 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 field_consumer; +mod field_producer; +mod method; 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, }; pub use runtime::{LolaRuntimeImpl, RuntimeBuilderImpl}; -use core::fmt::Debug; +pub use method::{ + 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 new file mode 100644 index 000000000..c2b73f859 --- /dev/null +++ b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/method.rs @@ -0,0 +1,240 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +// 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::fmt::Debug; +use core::future::Future; +use core::ops::Deref; +use score_com_concept::{ + CommData, MethodArgs, MethodArgsAllocate, MethodArgsPtrTuple, MethodCaller, MethodHandler, + MethodHandlerCall, MethodInArgAllocator, MethodInArgMaybeUninit, MethodInArgPtr, + MethodReturnSample, Result, Runtime, ZeroCopyArgs, +}; + +/// 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 + } +} + +impl MethodReturnSample for LolaMethodReturnSample {} + +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 + where + Args: MethodArgsPtrTuple, + { + 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, +} + +/// 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 { + type Ptr = LolaMethodInArgPtr; + + fn write(self, _val: T) -> ZeroCopyArgs> { + todo!("Implement write into Lola shared-memory slot"); + } + + unsafe fn assume_init(self) -> ZeroCopyArgs> { + todo!("Implement assume_init for Lola shared-memory slot"); + } +} + +// Lola placeholder allocator. +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"); + } +} + +/// 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/producer.rs b/score/mw/com/impl/rust/com-api/com-api-runtime-lola/producer.rs index ad172f54e..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}; @@ -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..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,17 +11,19 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -use crate::Debug; +use core::fmt::Debug; use core::marker::PhantomData; use std::path::{Path, PathBuf}; use crate::{ - LolaConsumerDiscovery, LolaConsumerInfo, LolaProducerBuilder, LolaProviderInfo, LolaPublisher, - LolaSubscribableImpl, + LolaConsumerDiscovery, LolaConsumerInfo, LolaFieldGetCaller, LolaFieldPublisher, + LolaFieldSetCaller, LolaFieldSubscriber, LolaMethodCaller, LolaMethodHandler, + LolaMethodInArgAllocator, LolaMethodReturnSample, 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 +38,14 @@ impl Runtime for LolaRuntimeImpl { type Subscriber = LolaSubscribableImpl; type ProducerBuilder = LolaProducerBuilder; type Publisher = LolaPublisher; + type MethodInArgAllocator = LolaMethodInArgAllocator; + type MethodReturnSample = LolaMethodReturnSample; + type MethodCaller = LolaMethodCaller; + 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 fceb5b082..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 @@ -36,10 +36,14 @@ 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, MethodArgs, MethodArgsAllocate, MethodArgsPtrTuple, MethodCaller, + MethodHandler, MethodHandlerCall, MethodInArgAllocator, MethodInArgMaybeUninit, MethodInArgPtr, + MethodReturnSample, Producer, ProducerBuilder, ProviderInfo, Publisher, + Result, Runtime, RuntimeBuilder, Sample, SampleContainer, + SampleMaybeUninit as SampleMaybeUninitTrait, SampleMaybeUninit, SampleMut, ServiceDiscovery, + Subscriber, Subscription, ZeroCopyArgs, }; pub struct MockRuntimeImpl {} @@ -69,6 +73,14 @@ impl Runtime for MockRuntimeImpl { type Subscriber = MockSubscribableImpl; type ProducerBuilder = MockProducerBuilder; type Publisher = MockPublisher; + type MethodInArgAllocator = MockMethodInArgAllocator; + type MethodReturnSample = MockMethodReturnSample; + 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; @@ -191,7 +203,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 +521,312 @@ 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)>, +} + +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"); + } +} + +/// Placeholder return sample for a mock method call result. +/// Wraps the return value and provides `Deref` 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 + } +} + +impl MethodReturnSample for MockMethodReturnSample {} + +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 + where + Args: MethodArgsPtrTuple, + { + async move { + todo!("Implement the logic to call the method with pre-allocated argument pointers") + } + } +} + +/// 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. +/// 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 { + 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!() + } +} + +/// 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: &'static 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(&self, _callback: impl Fn(T) + Send + 'static) { + todo!() + } + + fn register_get_handler(&self, _callback: impl Fn() -> T + Send + 'static) { + 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() { 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/design_document_method.md b/score/mw/com/rust/design/design_document_method.md new file mode 100644 index 000000000..a3beab12e --- /dev/null +++ b/score/mw/com/rust/design/design_document_method.md @@ -0,0 +1,516 @@ + +# 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 + +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. +- 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 + where + Args: MethodArgsPtrTuple; +} +``` + +`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` + +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 bounded by this trait: + +```rust +type MethodReturnSample: MethodReturnSample; +``` + +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 +``` + +#### `MethodInArgAllocator` + +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 { + /// 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; +} +``` + +#### `MethodInArgMaybeUninit` + +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 { + /// 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) -> ZeroCopyArgs; +} +``` + +#### `MethodInArgPtr` + +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 trait MethodInArgPtr {} +``` + +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 + +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. 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 MethodArgsPtrTuple: MethodArgs { + type PtrTuple; +} +// e.g. for R = LolaRuntime: +// (Tire, Tire)::PtrTuple = (ZeroCopyArgs>, ZeroCopyArgs>) +``` + +#### `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 `(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 { + 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 — ZeroCopyArgs 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` | 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 via `ZeroCopyArgs` tuples | +| `MethodHandlerCall` | Handler `call()` unpacks tuple per arity | +| `Reloc` | Arg tuple is relocatable 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. + +--- + +## 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 — returns ZeroCopyArgs> +let tire_ptr = uninit.write(Tire { pressure: 35.0 }); +// PtrTuple = (ZeroCopyArgs>,) — MethodCallInput zero-copy impl — 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 — Implement RAII lifecycle on `MethodInArgPtr` concrete types + +`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: + +### 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/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/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..246fef05c --- /dev/null +++ b/score/mw/com/rust/design/method_trait_diagram.puml @@ -0,0 +1,149 @@ +@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: >::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 = (ZeroCopyArgs>, ...) + ' Separated from MethodArgs because pointer types are runtime-specific +} + +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 (ZeroCopyArgs, ...) (dispatches invoke_zero_copy) +} + +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 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 + --- + ' Mirrors SampleMaybeUninit in the event design +} + +interface "MethodInArgPtr" as MethodInArgPtr { + --- + ' Trait - mirrors SampleMut in the event design + ' Runtime implements on its concrete type: + ' LolaMethodInArgPtr, MockMethodInArgPtr + ' Concrete type will hold FFI slot pointer + Drop +} + +class "ZeroCopyArgs

" as ZeroCopyArgs { + + 0: P + --- + ' 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 +Runtime --> MethodHandler : defines as\nassociated type +Runtime --> MethodInArgAllocator : defines as\nassociated type +Runtime --> MethodReturnSample : defines as\nassociated type + +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 --> ZeroCopyArgs : write() returns +ZeroCopyArgs --> MethodInArgPtr : wraps + +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..2295540ca --- /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 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 d16ae15b1..06c8439f1 100644 --- a/score/mw/com/rust/score_com.rs +++ b/score/mw/com/rust/score_com.rs @@ -130,16 +130,26 @@ //! # 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 development 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; 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, + HandlerNotSet, HandlerSet, Init, 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, ServiceDiscovery, + Subscriber, Subscription, Uninit, WithGetter, WithNotifier, WithSetter, ZeroCopyArgs, }; #[doc(hidden)] diff --git a/score/mw/com/rust/score_com_concept/BUILD b/score/mw/com/rust/score_com_concept/BUILD index 2f557d48e..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( @@ -55,7 +58,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"], 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 8e5f7601b..bfe46220a 100644 --- a/score/mw/com/rust/score_com_concept/concept.rs +++ b/score/mw/com/rust/score_com_concept/concept.rs @@ -49,13 +49,17 @@ //! - Tuples use crate::error::*; +use crate::field_concept::{FieldPublisher, FieldSubscriber}; +use crate::method_concept::{ + MethodArgs, MethodCaller, MethodHandler, MethodInArgAllocator, MethodReturnSample, +}; 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 +104,36 @@ 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. + type MethodReturnSample: MethodReturnSample; + + /// `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; + + /// `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; + /// `ProviderInfo` types for Configuration data for service producers instances type ProviderInfo: ProviderInfo + Send + Clone; @@ -210,6 +244,11 @@ pub trait CommData: Reloc { const ID: &'static str; } +// 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 = "()"; +} + /// 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 @@ -330,6 +369,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 +522,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..deb262c45 --- /dev/null +++ b/score/mw/com/rust/score_com_concept/field_concept.rs @@ -0,0 +1,123 @@ +/******************************************************************************** + * 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. +// +// 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::concept::{self, CommData, Result, Runtime, SampleMaybeUninit}; +use std::fmt::Debug; + +/// `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> +{ +} + +/// `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 that + /// `max_num_samples` does not restrict it) would currently provide. + fn get_num_new_samples_available(&self) -> Result; + + /// Returns the number of sample slots that can still be filled before the subscription + /// buffer overflows. + fn get_free_sample_count(&self) -> 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: &'static str, instance_info: R::ProviderInfo) -> Result + where + Self: Sized; + + /// Get the allocated sample ptr for the field publisher. + fn allocate(&self) -> 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 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` - 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. + /// + /// 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. +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_consumer_macros.rs b/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs new file mode 100644 index 000000000..ca313c128 --- /dev/null +++ b/score/mw/com/rust/score_com_concept/interface_consumer_macros.rs @@ -0,0 +1,272 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +/// 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) + )), + )+ + } + } + } + } + }; +} + +/// 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_macros.rs index 46cae701f..9f057acf6 100644 --- a/score/mw/com/rust/score_com_concept/interface_macros.rs +++ b/score/mw/com/rust/score_com_concept/interface_macros.rs @@ -11,29 +11,82 @@ * 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 +/// - `{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; @@ -53,8 +106,9 @@ /// "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: Event-only with custom ID /// ```ignore /// mod abc { /// use score_com::interface; @@ -67,7 +121,7 @@ /// ); /// } /// ``` -/// Here Id is explicitly set to "AbcInterface" instead of the default "abc::Vehicle". +/// 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 @@ -76,6 +130,57 @@ /// "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 @@ -102,24 +207,341 @@ macro_rules! interface { }) => { $crate::interface! { interface $id { - Id = $uid, - $($event_name : Event<$event_type>),+ - }} + 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)* + ); }; - (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: 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)* ); }; +} - (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." +/// 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. @@ -154,104 +576,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. -#[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) - )), - )+ - } - } - } - } - }; -} - -/// 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. -#[macro_export] -macro_rules! interface_producer { - ($id:ident, $($event_name:ident, Event<$event_type:ty>),+$(,)?) => { - score_com::paste::paste! { - pub struct [<$id Producer>] { - _runtime: core::marker::PhantomData, - instance_info: R::ProviderInfo, - } - - pub struct [<$id OfferedProducer>] { - $( - pub $event_name: R::Publisher<$event_type>, - )+ - instance_info: R::ProviderInfo, - } - - impl score_com::Producer for [<$id Producer>] { - type Interface = [<$id Interface>]; - type OfferedProducer = [<$id OfferedProducer>]; - fn offer(self) -> score_com::Result { - let offered = [<$id OfferedProducer>] { - $( - $event_name: R::Publisher::new( - stringify!($event_name), - self.instance_info.clone() - ).expect(&format!( - "Failed to create publisher for {}", - stringify!($event_name) - )), - )+ - instance_info: self.instance_info.clone(), - }; - // Offer the service instance to make it discoverable - self.instance_info.offer_service()?; - Ok(offered) - } - - fn new(instance_info: R::ProviderInfo) -> score_com::Result { - Ok([<$id Producer>] { - _runtime: core::marker::PhantomData, - 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>] { - _runtime: core::marker::PhantomData, - instance_info: self.instance_info.clone(), - }; - // Stop offering the service instance to withdraw it from system availability - self.instance_info.stop_offer_service()?; - Ok(producer) - } - } - } - }; -} - mod tests { /// ``` /// mod my_module { @@ -353,9 +677,110 @@ mod tests { #[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, ProviderInfo, Subscriber, Publisher}; + /// 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)] @@ -364,26 +789,249 @@ mod tests { /// 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 Exhaust {} - /// impl CommData for Exhaust { - /// const ID: &'static str = "Exhaust"; + /// pub struct Tire { pub pressure: f32 } + /// impl CommData for Tire { + /// const ID: &'static str = "Tire"; /// } /// /// interface!( /// interface Vehicle { - /// Id = "CustomVehicleInterface", - /// left_tire: Method, - /// exhaust: Method, + /// 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, /// } /// ); /// } /// ``` - /// 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: + /// - `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_with_Method() {} + 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 { @@ -405,14 +1053,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() {} @@ -749,7 +1419,7 @@ mod validation_tests { const ID: &'static str = "Tire"; } - crate::interface!( + score_com::interface!( interface Vehicle { left_tire: Event, } @@ -795,7 +1465,7 @@ mod validation_tests { const ID: &'static str = "Exhaust"; } - crate::interface!( + score_com::interface!( interface Vehicle { left_tire: Event, exhaust: Event, @@ -831,7 +1501,7 @@ mod validation_tests { const ID: &'static str = "Tire"; } - crate::interface!( + score_com::interface!( interface Engine { rpm: Event, } @@ -862,7 +1532,7 @@ mod validation_tests { const ID: &'static str = "Tire"; } - crate::interface!( + score_com::interface!( interface Transmission { gear: Event, } @@ -895,7 +1565,7 @@ mod validation_tests { const ID: &'static str = "Tire"; } - crate::interface!( + score_com::interface!( interface Battery, { Id = "com.example.Battery", voltage: Event, @@ -950,7 +1620,7 @@ mod validation_tests { const ID: &'static str = "Event3Data"; } - crate::interface!( + score_com::interface!( interface MultiEvent { event_one: Event, event_two: Event, @@ -994,7 +1664,7 @@ mod validation_tests { const ID: &'static str = "Tire"; } - crate::interface!( + score_com::interface!( interface Suspension { travel: Event, } @@ -1036,7 +1706,7 @@ mod validation_tests { const ID: &'static str = "Data"; } - crate::interface!( + score_com::interface!( interface ABS { status: Event, } 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 new file mode 100644 index 000000000..090e3a40a --- /dev/null +++ b/score/mw/com/rust/score_com_concept/interface_producer_macros.rs @@ -0,0 +1,255 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +// 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 +/// 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>),+$(,)?) => { + score_com::paste::paste! { + pub struct [<$id Producer>] { + _runtime: core::marker::PhantomData, + instance_info: R::ProviderInfo, + } + + pub struct [<$id OfferedProducer>] { + $( + pub $event_name: R::Publisher<$event_type>, + )+ + instance_info: R::ProviderInfo, + } + + impl score_com::Producer for [<$id Producer>] { + type Interface = [<$id Interface>]; + type OfferedProducer = [<$id OfferedProducer>]; + fn offer(self) -> score_com::Result { + let offered = [<$id OfferedProducer>] { + $( + $event_name: R::Publisher::new( + stringify!($event_name), + self.instance_info.clone() + ).expect(&format!( + "Failed to create publisher for {}", + stringify!($event_name) + )), + )+ + instance_info: self.instance_info.clone(), + }; + // Offer the service instance to make it discoverable + self.instance_info.offer_service()?; + Ok(offered) + } + + fn new(instance_info: R::ProviderInfo) -> score_com::Result { + Ok([<$id Producer>] { + _runtime: core::marker::PhantomData, + 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>] { + _runtime: core::marker::PhantomData, + instance_info: self.instance_info.clone(), + }; + // Stop offering the service instance to withdraw it from system availability + self.instance_info.stop_offer_service()?; + Ok(producer) + } + } + } + }; +} + +/// 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[], + fields_setter[], + fields_getter[], + 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 ,)*], + 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()`. + // Fields: FieldPublisher per field + MethodHandler per method. + // 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>, + )* + $( + $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, + }) + } + } + } + }; +} diff --git a/score/mw/com/rust/score_com_concept/lib.rs b/score/mw/com/rust/score_com_concept/lib.rs index 920c9b7bb..b3e848512 100644 --- a/score/mw/com/rust/score_com_concept/lib.rs +++ b/score/mw/com/rust/score_com_concept/lib.rs @@ -22,10 +22,22 @@ /// boundaries without violating Rust's ownership rules. 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; 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 method_concept::*; #[doc(hidden)] pub use paste; pub use reloc::Reloc; +#[doc(hidden)] +pub use score_com_macros; 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..82fc8f79c --- /dev/null +++ b/score/mw/com/rust/score_com_concept/method_arities_macros.rs @@ -0,0 +1,155 @@ +/******************************************************************************** + * 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`, `MethodArgsPtrTuple`, `MethodArgsAllocate`, `MethodCallInput` (zero-copy +//! path), and `MethodHandlerCall` - is generated using macros. +//! +//! 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 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 +//! +//! 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. + +use crate::{ + CommData, MethodArgs, MethodArgsAllocate, MethodArgsPtrTuple, MethodCallInput, MethodCaller, + MethodHandlerCall, MethodInArgAllocator, Reloc, Result, Runtime, ZeroCopyArgs, +}; +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,) {} + + 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> + 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 `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 ($( 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, + caller: &'a R::MethodCaller<($($T,)* $nextT,), Return>, + ) -> impl Future>> + 'a + where + R::MethodCaller<($($T,)* $nextT,), Return>: + MethodCaller<($($T,)* $nextT,), Return, R> + 'a, + { + // `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, + self, + ) + } + } + impl 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: +// 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. +// +// 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/method_concept.rs b/score/mw/com/rust/score_com_concept/method_concept.rs new file mode 100644 index 000000000..562a093aa --- /dev/null +++ b/score/mw/com/rust/score_com_concept/method_concept.rs @@ -0,0 +1,365 @@ +/******************************************************************************** + * 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, +/// 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–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 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. +/// +/// 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. +/// `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, +/// 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, +/// 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. +/// +/// 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 to `futures::executor::block_on`). +use crate::concept::{CommData, Result, Runtime}; +use core::future::Future; +use core::ops::Deref; + +/// 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. +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 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. + /// + /// 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` - 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 + where + Args: MethodArgsPtrTuple; +} + +/// 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. +pub trait MethodInArgMaybeUninit { + /// The runtime-specific concrete pointer type produced after initialisation. + // Mirrors `SampleMaybeUninit::SampleMut` in the event design. + type Ptr: MethodInArgPtr; + + /// 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) -> 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< + T, + Ptr = Self::MethodInArgPtr, + >; + + /// 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. +/// +/// Runtimes do not implement this trait. +/// Blanket impls for all supported arities (0–8 arguments) are provided in this crate. +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 - 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 = (); +} + +/// 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_macros.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) +/// - `(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. +/// +/// 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>`, + /// providing `Deref` access to the return value. + 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. +/// 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. +/// 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_macros.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/reloc.rs b/score/mw/com/rust/score_com_concept/reloc.rs index 4e751ae40..777f1dbcb 100644 --- a/score/mw/com/rust/score_com_concept/reloc.rs +++ b/score/mw/com/rust/score_com_concept/reloc.rs @@ -55,9 +55,4 @@ 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) {} +// Tuples generated by `impl_all_arities!` in `method_arities_macros.rs` 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..cd3d9f08e 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,62 @@ fn collect_field_types(data: &Data) -> Result, ()> { Ok(out) } +/// 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()`. +/// +/// 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 +/// +/// `{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. +// Once field or method design merged, other PR can add the tests for this macro. +#[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) +} + // 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..4b953f859 --- /dev/null +++ b/score/mw/com/rust/score_com_macros/type_state_validator.rs @@ -0,0 +1,516 @@ +/******************************************************************************** + * 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 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) 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`) - 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 `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`). +/// +/// 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 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 }) + }; + + // 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, + _ => { + return syn::Error::new_spanned( + name, + "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, "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 + has_setter: bool, + has_getter: bool, + } + 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 `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() { + "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(), + }); + } + } + } + "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, + "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) - 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(); + + // 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(|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..Hm, G0..Gk, M0..Mp] + let all_params: Vec<&syn::Ident> = field_update_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..n_fields) + .map(|_| quote! { ::score_com::Uninit }) + .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..n_fields) + .map(|_| quote! { ::score_com::Init }) + .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. + // 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 - 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(|(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 hj_index = n_fields + j; + + let after: Vec<_> = all_params + .iter() + .enumerate() + .map(|(k, p)| { + if k == hj_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_set_handler(handler); + #validator_name { + producer: self.producer, + _phantom: core::marker::PhantomData, + } + } + } + } + }) + .collect(); + + // 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(|(p, 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 mp_index = n_fields + n_setters + n_getters + p; + + let after: Vec<_> = all_params + .iter() + .enumerate() + .map(|(k, p_param)| { + if k == mp_index { + quote! { ::score_com::HandlerSet } + } else { + quote! { #p_param } + } + }) + .collect(); + + quote! { + impl<#runtime_param_with_bounds, #(#all_params),*> + #validator_name<#runtime_param_name, #(#all_params),*> + { + pub fn #register_fn( + 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 (all fields) + #(#update_methods)* + + // register_set_handler_{name}() - transitions Hj: HandlerNotSet - HandlerSet (WithSetter fields only) + #(#register_set_handler_methods)* + + // 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 Hj = HandlerSet, + // ALL Gk = HandlerSet, ALL Mp = 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) +}