Skip to content

created initial rust based design draft for mw::diag#78

Draft
MonikaKumari26 wants to merge 1 commit intoeclipse-opensovd:mainfrom
MonikaKumari26:rust_abstract_layer_api_for_user_code
Draft

created initial rust based design draft for mw::diag#78
MonikaKumari26 wants to merge 1 commit intoeclipse-opensovd:mainfrom
MonikaKumari26:rust_abstract_layer_api_for_user_code

Conversation

@MonikaKumari26
Copy link

  • I have tested my changes locally
  • I have added or updated documentation
  • I have linked related issues or discussions
  • I have added or updated tests

Related

Notes for Reviewers

{abstract} +unlock(&mut self) -> Result<()>
{abstract} +kind(&self) -> DiagnosticEntityKind
{abstract} +supported_modes(&self) -> Result<Vec<DiagnosticEntityMode>>
{abstract} +apply_mode(&mut self, mode_id: &str, mode_value: &str, expiration_timeout: Option<Duration>) -> Result<()>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we were discussing async APIs, maybe it would make sense here to use one

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to be async since applying a diagnostic mode might take time. supported_modes() could be also async if querying supported modes requires reading from vehicle systems.

+request_results(&self, params: ByteSlice) -> Result<ByteVec>
}

enum UDSResponseCode {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the assigned enumerator values are not really rust-compliant, am I right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assigned values to match the protocol's hex codes, but anyway I have removed the manual assignments and let the compiler handle the discriminant, which is much more idiomatic for Rust. I have added as_byte() and from_byte() which can handle the conversion.

Comment on lines 437 to 439
+get_write_did_mut(&mut self, id: &str) -> Option<&mut dyn WriteDataByIdentifier>
+get_routine_mut(&mut self, id: &str) -> Option<&mut dyn RoutineControl>
+get_service_mut(&mut self, id: &str) -> Option<&mut dyn UdsService>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why mut?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this provides clear distinction between read-only and mutating operations. Here we need &mut self and return &mut dyn Trait for several UDS methods because the underlying trait methods require mutable access to implement their functionality correctly.
- get_write_did_mut: WriteDataByIdentifier::write() mutates state
- get_routine_mut: RoutineControl::start()/stop() mutate state
- get_service_mut: UdsService::handle_message() mutates state
As per Rust borrow rules: returning &mut T requires &mut self.

Copy link
Contributor

@lh-sag lh-sag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MonikaKumari26 thanks for your contribution!

type DataGroupId = String
}

entity JsonSchema {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can actually be a json type.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed from String to JsonValue (which is serde_json::Value).

}
}

package "mw::diag::uds" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be good to have this either as a separate create or guard it with a feature flag. There will be SOVD uses who do not want to use this feature.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its good idea. I have updated the design to use cargo feature flags for modularity, SOVD-only users get smaller binaries (no UDS code compiled). Maintaining single crate keeps dependency management simple.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be good to have this either as a separate create or guard it with a feature flag. There will be SOVD uses who do not want to use this feature.

That's a great idea!

@MonikaKumari26 MonikaKumari26 force-pushed the rust_abstract_layer_api_for_user_code branch 2 times, most recently from a87978f to e9e5e95 Compare February 5, 2026 11:38
@lh-sag lh-sag self-requested a review February 5, 2026 19:47
@lh-sag
Copy link
Contributor

lh-sag commented Feb 5, 2026

@MonikaKumari26 I suggest to move this PR out of draft status to get more feedback.

@MonikaKumari26 MonikaKumari26 force-pushed the rust_abstract_layer_api_for_user_code branch from e9e5e95 to 82693a9 Compare February 6, 2026 12:29
Copy link

@darkwisebear darkwisebear left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not done, but maybe these comments are helpful still. One general comment: This library allocates a lot. We need to see whether we can cut back on allocations (or avoid them somehow?) and we need to introduce an Allocator trait, as the canonical one from std isn't stabilized yet.


fn register_vin_reader() -> Result<DiagnosticServicesCollection> {
DiagnosticServicesCollectionBuilder::new()
.with_read_did("0xF190", VinReader)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is deployment specific information. Would it make sense to move this to a config file so that here, we only give an abstract identifier?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have replaced hard coded values with configuration variables just to demonstrate the pattern. changes have been pushed.


fn register_config_writer() -> Result<DiagnosticServicesCollection> {
DiagnosticServicesCollectionBuilder::new()
.with_write_did("0xF191", ConfigWriter)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swapped hard coded values for config variables.


fn register_my_routine() -> Result<DiagnosticServicesCollection> {
DiagnosticServicesCollectionBuilder::new()
.with_routine("0xFF00", MyRoutine { state: 0 })

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes are up.

ReadOnlyDataResource, DiagnosticServicesCollectionBuilder,
DiagnosticEntity, JsonDataReply
};
use opensovd_diag::sovd::types::SOVDResult;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use opensovd_diag::sovd::types::SOVDResult;
use opensovd_diag::sovd::Result;

I think we should still use the canonical Result type even for custom Result aliases, plus we should not expose an additional types module but (re-)export it from sovd. If one wanted to distinguish between std::result::Result and SOVD Result, they could simply use opensovd_diag::sovd; and use sovd::Result.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the canonical Result type while re-exporting it from the sovd module keeps the API much cleaner. I removed the separate "types" module exposure and set up the re-export so users can simply use sovd::Result. Considered this in puml file as well.

-vendor_message: String
}

class "Result<T>" as SovdResult {
Copy link

@darkwisebear darkwisebear Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class "Result<T>" as SovdResult {
class "Result<T>" as Result {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have refactored to use the canonical Result and updated the exports.

Both can be enabled independently or together.
end note

class SovdError {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class SovdError {
class Error {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considered previous changes for this also.

Comment on lines 233 to 236
+with_read_resource<T: ReadOnlyDataResource + Send + Sync + 'static>(self, id: impl Into<String>, resource: T) -> Self
+with_write_resource<T: WritableDataResource + Send + Sync + 'static>(self, id: impl Into<String>, resource: T) -> Self
+with_data_resource<T: DataResource + Send + Sync + 'static>(self, id: impl Into<String>, resource: T) -> Self
+with_operation<T: Operation + Send + Sync + 'static>(self, id: impl Into<String>, operation: T) -> Self

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for these types to be Sync?

Copy link
Author

@MonikaKumari26 MonikaKumari26 Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that these references must be Sync to allow concurrent read access from multiple threads, ensuring safe shared diagnostic operations and no compiler errors when shared via &T.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that'd only be relevant if I wanted to execute the same operation concurrently. Is that really the case? Imo executing an operation should be exclusive, which means that T needs to be Send (so that an instance of T can be executed in different threads), but not Sync (because no concurrent execution is required).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the concurrency constraints, I initially used uniform Send + Sync requirement for simplicity, but I have now refined this to be more precise based on resource behavior:

  1. Operation Trait: Retained Send + Sync. Since it exposes both &self and &mut self methods, Sync is required to allow concurrent status queries via immutable references.
  2. Write-only resources/services, RoutineControl and UdsService to be Send only, as they don't benefit from concurrent access.
  3. Read-only Resources: Retained Send + Sync to explicitly support concurrent reads.

Design may need further adjustments if these Sync requirements conflict with specific downstream use cases.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

&self / &mut self are static ownership properties that help the compiler to find out whether it's possible to allow for modifications of the instance state in a certain context (imo &mut should have been called &unique).

In conjunction with concurrency traits, it helps the compiler to know whether copying a reference across thread boundaries preserves these properties. However, in case of e.g. Operation types, even if they're Sync, the method signature of e.g. execute requires &mut self, so you statically demand a unique ownership. This either requires a mutex or something similar to go from shared to unique ownership if this exclusivity is enforced at runtime, or even a static unique ownership, which is hard to obtain unless you completely handle all diag events in a single thread. But since you already demand unique ownership, requiring the type to be Sync is asked too much as you do not need to synchronize anyway if ownership is already unique.

The bottom line for me is that if we have &mut self methods, requiring Sync doesn't help at all. Sync only makes sense if most / all methods are &self so that you can call them from multiple threads truly concurrently. Deciding this means to think whether synchronization shall be done by all implementors of Operation and the like (which makes these types Sync, but allows for fine grained synchronization if required), or whether the framework will take care, which makes implementing the diagnostic structs easier, but the synchronization will be more coarse grained.

end note

class "DiagnosticServicesCollectionBuilder<'entity>" as SovdDiagnosticServicesCollectionBuilder {
+new(entity: &'entity dyn DiagnosticEntity) -> Self

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This signature requires the user to hand over a borrow. While this sounds off, to increase the flexibility, here it makes sense to acquire ownership of the entity. This way, any type (and not just references) could be used here, which includes references, but also things like direct ownership or using an Arc<dyn DiagnosticEntity> which might in practice be the one with the best usability.

If you want the type to also work with a non-'static lifetime, you can do it like that:

class "DiagnosticServicesCollectionBuilder<'a, T: DiagnosticEntity + 'a>" as SovdDiagnosticServicesCollectionBuilder {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@darkwisebear so you are suggesting that the builder shall own the DiagnosticEntity in some regard? The intention was actually that the DiagnosticEntity owns the DiagnosticServicesCollection which itself just has a reference back to the DiagnosticEntity

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eww, but isn't that a circular dependency and we all hate them for good reasons?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so here's a proposal on how we could resolve the situation. Assumptions for the proposed design:

  • Type erasure shall be done so that DiagnosticServicesCollection / DiagnosticServicesCollectionBuilder do not need type parameters.
  • The user shall be able to build a DiagnosticServicesCollection using either a type implementing DiagnosticEntity, or some reference to a type implementing DiagnosticEntity.

The following changes would need to be applied:

  • entity: &'entity dyn DiagnosticEntityentity: Box<dyn DiagnosticEntity + 'entity>. This way, DiagnosticServicesCollection can store any type that implements DiagnosticEntity by value. Since the trait bound also has a lifetime bound, we can still use references whose lifetimes get tracked by the compiler, like & or Cow or the like.
  • As we will provide an impl DiagnosticEntity for &'_ T, Arc<T>, Weak<T> (where the latter will return an error in case the instance already got deleted), the user may hand over any of those types to the DiagnosticServicesCollectionBuilder.
  • new(entity: &'entity dyn DiagnosticEntity) -> Selfnew<T: DiagnosticEntity + 'entity>(entity: T) -> Self to accept any type that's suitable. The new implementation will then box the given entity and do the type erasure.

Copy link
Author

@MonikaKumari26 MonikaKumari26 Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this suggestion.. I am pivoting to the Box<dyn DiagnosticEntity + 'entity> approach to allow flexible ownership patterns and avoid the entity holding a reference to the collection. Based on my understanding, the user is now responsible for choosing their storage strategy (owned, &T, Arc, Weak) by selecting which type they pass to new().

To ensure I am on the right track, a few clarifications :

  1. The change from &'entity dyn DiagnosticEntity to Box<dyn DiagnosticEntity + 'entity> adds heap allocation. Is this acceptable for the performance requirements?

  2. For the Blanket Implementations: Should impl DiagnosticEntity for &T/Arc/Weak be:

  • Provided by the crate (mandatory for all three)
  • Optional via feature flags (to enable /disable them)
  • Just documented as examples for users to implement (Currently I have documented them as examples).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MonikaKumari26 My comments on your questions:

  1. That's acceptable since this is happening during initialization. If this turned out to be a problem, we could replace this by smallbox.
  2. These implementations should be provided by the crate. I don't think that this needs a feature flag since these are smallish implementations that do not add overhead if not used and also shouldn't contribute to compile times / rlib sizes that much.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarifications.. I have updated the Design Goals document to reflect these points.

@MonikaKumari26 MonikaKumari26 force-pushed the rust_abstract_layer_api_for_user_code branch from 82693a9 to 887af25 Compare February 11, 2026 08:56
}

fn register_vin_reader(did_identifier: &str) -> Result<DiagnosticServicesCollection> {
// did_identifier would come from configuration (e.g., "0xF190" for VIN)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather have something like "VehicleIdentificationNumber", with a deployment specific mapping to some UDS ID. Just putting the hex number in a string isn't cutting it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.. I have done it in rust-idiomatic way to move away from raw hex strings. Added VehicleDataIdentifier enum with semantic names and implemented an as_uds_did() method to handle the mapping to specific UDS IDs.

@MonikaKumari26 MonikaKumari26 force-pushed the rust_abstract_layer_api_for_user_code branch from 887af25 to 7825f43 Compare February 13, 2026 12:42
Copy link
Contributor

@lh-sag lh-sag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MonikaKumari26 if you want broader feedback I would advise you to move this PR from draft to final.

}

impl WritableDataResource for ConfigResource {
fn put(&mut self, request: DiagnosticRequest) -> sovd::Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO get/set is the common nomenclature.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have deliberately chosen get/put to align with HTTP/REST conventions. GET for read operation
and PUT for the update/write operation.

Copy link
Contributor

@lh-sag lh-sag Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For operation you chose e.g. stop() instead of DELETE. Should the name not match the intent rather than the HTTP method? The latter is terminology from the transport protocol. This would make the naming schema consistent e.g. read/write, start/stop etc

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right regarding the consistency of the naming convention. I will rename get() to read() and put() to write() to align with the existing UDS API patterns (ReadDataByIdentifier::read() and WriteDataByIdentifier::write()). This transition ensures the naming is intent-based rather than tied to specific transport protocols.

<<Debug, Clone>>
+new(data: JsonValue) -> Self

+with_headers(self, headers: Headers) -> Self
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure whether we should send http headers as-is to the diaglib. Instead we can have properties one can send along. Header could contain credentials/tokens and not needed data. I would therefore opt for generic properties instead.

Copy link
Author

@MonikaKumari26 MonikaKumari26 Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I will replace the headers field with a generic properties: IndexMap<String, String>. This lets us filter out credentials/tokens and only pass along relevant, safe metadata. But this makes the API protocol-independent. Is this approach acceptable or not sure, are there SOVD specific requirements that would need HTTP header access?

+build(self) -> Result<DiagnosticServicesCollection<'entity>>
--
-entity: Box<dyn DiagnosticEntity + Send + Sync + 'entity>
-read_resources: HashMap<ResourceIdentifier, Box<dyn ReadOnlyDataResource + Send + Sync>>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to support or even care for a stable order we should consider IndexMap instead. This would allows for a stable result if one requests /v1/apps/app_id/data

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, very good point!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have switched from HashMap to IndexMap for deterministic iteration order.

entity: Arc<dyn DiagnosticEntity + Send + Sync + 'entity>
) -> sovd::Result<DiagnosticServicesCollection<'entity>> {
DiagnosticServicesCollectionBuilder::new(entity)
.with_data_resource("parameter", ParameterResource { value: 0 })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I miss the schema. Should this be provided by the Resource itself as it will will return the data for the request.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To address your schema concern, I have added schema parameters to the builder registration methods. This approach provides protocol compliance without mandating specific serialization patterns.


impl WritableDataResource for ParameterResource {
fn put(&mut self, request: DiagnosticRequest) -> sovd::Result<()> {
if let Some(value) = request.data.get("parameter").and_then(|v| v.as_i64()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good example. Please consider adding serde, which will provide better ergonomics.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted as future enhancement.


impl ReadOnlyDataResource for ParameterResource {
fn get(&self) -> sovd::Result<JsonDataReply> {
Ok(JsonDataReply::new(json!({ "parameter": self.value })))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With an indirection we could support type safety by requiring the type to be serializable. This would also improve api ergonomics.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this suggestion !! I understand the benefits of using generic serialization with type parameters. For the MVP, I have chosen to keep the current design. However, I agree this would improve ergonomics. I have noted this as a future enhancement.

- **Mixed traits** with both `&self` and `&mut self` methods (`DiagnosticEntity`, `Operation`, `DataIdentifier`, `DataResource`): Require `Send + Sync + 'static` because they have immutable methods that could be called concurrently (e.g., `Operation::info(&self)`, `Operation::status(&self)`)
- **Mutable-only traits** (`RoutineControl`, `UdsService`): Require only `Send + 'static` because they only have `&mut self` methods
- **Trait Objects**: All traits marked with `<<object-safe>>` can be used as `dyn Trait` (e.g., `Box<dyn ReadDataByIdentifier + Send + Sync>`)
- **Feature Flags**: Both "sovd" and "uds" modules are optional and independently toggleable via Cargo features
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would assert that at least one feature is set. And do you plan to have a default-feature e.g. sovd?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding default features, I have chosen not to set a default in order to align with our design philosophy (SOVD-first development: newly written applications should use native SOVD APIs instead of legacy UDS APIs). By requiring an explicit choice between sovd, uds or both, we ensure that developers prioritize native SOVD APIs. From a technical standpoint, this 'opt-in' model allows users to minimize binary size and improve compilation speeds by including only the code they need.

To ensure the crate remains functional, I have implemented a compile-time assertion to verify that at least one protocol feature is enabled.

**Important**: This design is intended for automated code generation. Key constraints:

- **Rust Edition**: 2021+ (minimum required)
- **Async Strategy**: All trait methods are currently **synchronous**. While shown in design for API clarity, the current implementation does not use async. Future versions may add async support using `#[async_trait]` macro.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO it is easier to have async fn first and support blocking on top. What is the rational to start with sync first?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the original statement in the document is too generic. We need to look at the methods one by one and determine their nature:

Operation

Most methods query state; these methods are nonblocking and there is imo no reason to make them async, not even in a second step, as such data acquisition should not require any long running activities. The same is true for the execute method: This one also isn't supposed to block. If it does need to do something that takes longer, it is supposed to spawn a concurrent task or thread and return with a pending state.

The only notable exception in my eyes is stop, because I could imagine that stopping a task might incur some waiting. I'd need to check the SOVD spec whether or not we'd need to delay the response of this method until the task really stopped.

ReadableDataResource

I think it makes sense to make the get call async, as it is absolutely thinkable that data acquisition will be done asynchronously by the implementation.

WritableDataResource

Here, the same argumentation as for the execute call holds, unless it is required by the spec to also tell whether setting the data actually happened (aka a positive response not just confirms data reception but also confirms proper changing of the inner state). In that case, the put method also needs to be async.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your implications about the system might fit for yours but we need an async interface for ours. It is just a matter of how we implement it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. Let's take Operation: If you opt for an API design (and that's how it looks) where you map SOVD requests 1:1 to API calls, the resulting API and the way how you implement the trait should give you an idea about what color your function is.

If you disagree with my assessments, please give your view of that particular method and why it should or shouldn't be async, or you also mention why the suggested API design philosophy (have a thin layer above the REST calls) is not the correct one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. And our operations/API is fully async this does not mean write will work differently as you suggested but our core is async. This is completely orthogonal to SOVD.

Please note that I do not enforce either way sync or async, just that we have an async interface as our system underneath work differently than yours.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo the API should already be usable for you as well without having to jump through hoops; either we will have a truly async method anyway (e.g. get of ReadableDataResource), or the methods are ordinary ones without suspension points, but these shall be (see my comment above) nonblocking, so that they can also be called in an async context. So even if you are fully async, this shouldn't prevent you from being able to implement this API.

On a side note, we shouldn't actually talk about "your system" or "our system". In the end we want to build something that can be used in many different contexts, and if there are contexts where the API is not a good fit, I'd need to understand what exactly isn't fitting so that we can develop a common solution that fits.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a side note, we shouldn't actually talk about "your system" or "our system". In the end we want to build something that can be used in many different contexts, and if there are contexts where the API is not a good fit, I'd need to understand what exactly isn't fitting so that we can develop a common solution that fits.

We do agree here. But since our systems differs we are biased, I doubt we would have argued if this is not the case. Therefore my initial remark was whether we have a fully sync and async interface.

Where we differ is having a mixed one e.g. I do not see why for example Operation::status should not be colored async. The question about async/sync interface is independent of how SOVD works.

I will join next weeks UDS2SOVD meeting in case you want to discuss this outside of this PR.


## Code Generation Guidelines

**Important**: This design is intended for automated code generation. Key constraints:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "intended for automated code generation"?

Copy link
Author

@MonikaKumari26 MonikaKumari26 Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It signifies that the logic defined here can be consumed by user to produce executable code. I have tried to ensures that the implementation aligned with the design specifications, reducing error during the coding phase.


class "SerializedDataByIdentifier<T>" as SerializedDataByIdentifier {
<<generic>>
where T: UdsSerialize + UdsDeserialize + Clone + Send + Sync + 'static
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why T has to be clone here ??


class "SerializedReadDataByIdentifier<T>" as SerializedReadDataByIdentifier {
<<generic>>
where T: UdsSerialize + Send + Sync + 'static
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does T need to be Send + Sync + 'static?

Copy link
Author

@MonikaKumari26 MonikaKumari26 Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Send looks sufficient because the collection may be moved between threads, but services are accessed sequentially. Sync will be removed as there is no concurrent access to services in practice. static required for type erasure.

Comment on lines +344 to +354
interface Operation <<trait>> {
API definition for SOVD operations

.. standard SOVD capabilities ..
{abstract} +async info(&self, request: DiagnosticRequest) -> Result<OperationInfoReply>
{abstract} +async status(&self, request: DiagnosticRequest) -> Result<OperationStatusReply>
{abstract} +async execute(&mut self, request: DiagnosticRequest) -> Result<ExecuteOperationReply>
{abstract} +async resume(&mut self, request: DiagnosticRequest) -> Result<ExecuteOperationReply>
{abstract} +async reset(&mut self, request: DiagnosticRequest) -> Result<ExecuteOperationReply>
{abstract} +async stop(&mut self, request: DiagnosticRequest) -> Result<()>

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API must follow synchronous non-blocking pattern here. The implementation should avoid the overhead of a multi-threaded async runtime while also ensuring functions do not halt execution while waiting. Instead, operations should return immediately with a status (WouldBlock or InProgress), requiring the caller to poll for completion.

Comment on lines +462 to +464
-start_handler: Box<dyn Fn(T) -> DiagResult<T> + Send + Sync>
-stop_handler: Box<dyn Fn(T) -> DiagResult<T> + Send + Sync>
-results_handler: Box<dyn Fn(T) -> DiagResult<T> + Send + Sync>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current design has three separate Box for handlers, which is inefficient. Instead, creating a single trait that the user implements, requiring only one Box.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants