diff --git a/Cargo.toml b/Cargo.toml index b3c25b8..d30c416 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,8 @@ members = [ "rustecal-samples/pubsub/hello_send", "rustecal-samples/pubsub/hello_receive", "rustecal-samples/pubsub/person_send", - "rustecal-samples/pubsub/person_receive"] + "rustecal-samples/pubsub/person_receive", + "rustecal-samples/service/mirror_client", + "rustecal-samples/service/mirror_client_instances", + "rustecal-samples/service/mirror_server" + ] diff --git a/README.md b/README.md index 956dd95..6b83e97 100644 --- a/README.md +++ b/README.md @@ -8,80 +8,117 @@ Safe and idiomatic Rust bindings for the [eCAL](https://github.com/eclipse-ecal/ ## Features -- ๐Ÿ“ก High-performance publish/subscribe middleware (based on eCAL) -- ๐Ÿฆ€ Idiomatic Rust API over eCAL C-API -- ๐Ÿ’ฌ Type-safe messaging for: - - `StringMessage` - - `BytesMessage` - - `ProtobufMessage` (based on `prost`) -- ๐Ÿงช Works on Linux and Windows (via `bindgen` + `cc`) -- ๐Ÿ“– Modular message support via `rustecal-types-*` crates +- Idiomatic Rust interface to the eCAL C API +- Zero-copy shared memory transport +- Type-safe publish/subscribe and service communication +- Modular type support: String, Binary, Protobuf +- Fully runtime-compatible with C++ eCAL systems --- -## Usage +## Examples -Add the following to your `Cargo.toml`: - -```toml -[dependencies] -rustecal = { path = "path/to/rustecal" } -rustecal-types-string = { path = "path/to/rustecal-types-string" } -``` - -Example (sending a string message): +### Publisher ```rust +use rustecal::{Ecal, EcalComponents}; use rustecal::pubsub::Publisher; -use rustecal::types::StringMessage; -let _ecal = rustecal::Ecal::initialize("hello_send")?; +fn main() -> Result<(), Box> { + Ecal::initialize(Some("rust publisher"), EcalComponents::DEFAULT)?; + let mut pub = Publisher::::new("chatter")?; -let publisher = Publisher::::builder("hello_topic").create()?; -publisher.send("Hello from Rust!")?; + loop { + pub.send("Hello from Rust!")?; + std::thread::sleep(std::time::Duration::from_millis(500)); + } +} ``` -Example (receiving a message): +--- + +### Subscriber ```rust +use rustecal::{Ecal, EcalComponents}; use rustecal::pubsub::Subscriber; -use rustecal::types::StringMessage; -let _ecal = rustecal::Ecal::initialize("hello_receive")?; +fn main() -> Result<(), Box> { + Ecal::initialize(Some("rust subscriber"), EcalComponents::DEFAULT)?; + let sub = Subscriber::::new("chatter")?; + + sub.set_callback(|msg| { + println!("Received: {}", msg.payload); + })?; -let subscriber = Subscriber::::builder("hello_topic").create()?; -subscriber.set_callback(|msg| { - println!("Received: {}", msg.data()); -}); + loop { + std::thread::sleep(std::time::Duration::from_millis(100)); + } +} ``` --- -## Crate Structure +### Service Server -- `rustecal`: core eCAL bindings and idiomatic API -- `rustecal-sys`: low-level `bindgen` generated FFI bindings -- `rustecal-types-string`, `rustecal-types-bytes`, `rustecal-types-protobuf`: message wrapper crates +```rust +use rustecal::{Ecal, EcalComponents}; +use rustecal::service::server::ServiceServer; +use rustecal::service::types::MethodInfo; + +fn main() -> Result<(), Box> { + Ecal::initialize(Some("mirror server"), EcalComponents::DEFAULT)?; + let mut server = ServiceServer::new("mirror")?; + + server.add_method("reverse", Box::new(|_info: MethodInfo, req: &[u8]| { + let mut reversed = req.to_vec(); + reversed.reverse(); + reversed + }))?; + + loop { + std::thread::sleep(std::time::Duration::from_millis(100)); + } +} +``` --- -## Documentation - -๐Ÿ“š Full user guide: [https://rex-schilasky.github.io/rustecal](https://rex-schilasky.github.io/rustecal) +### Service Client -```bash -cd docs/ -mdbook serve +```rust +use rustecal::{Ecal, EcalComponents}; +use rustecal::service::client::ServiceClient; +use rustecal::service::types::ServiceRequest; + +fn main() -> Result<(), Box> { + Ecal::initialize(Some("mirror client"), EcalComponents::DEFAULT)?; + let client = ServiceClient::new("mirror")?; + + let request = ServiceRequest { + payload: b"stressed".to_vec(), + }; + + if let Some(response) = client.call("reverse", request, Some(1000)) { + println!("Reversed: {}", String::from_utf8_lossy(&response.payload)); + } else { + println!("No response received."); + } + + Ok(()) +} ``` --- -## License +## Documentation -Licensed under Apache-2.0 or MIT. +- ๐Ÿ“˜ API Docs: [docs.rs/rustecal](https://docs.rs/rustecal) +- ๐Ÿ“– Guide & Examples: see `docs/` (mdBook) --- -## Maintainer +## License -[Rex Schilasky](https://github.com/rex-schilasky) +Licensed under the Apache License 2.0 (see [LICENSE](./LICENSE)) +ยฉ 2024โ€“2025 Eclipse Contributors / Rex Schilasky diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 11bc4f2..ccd1308 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -14,5 +14,7 @@ - [Typed Publisher](api/publisher.md) - [Typed Subscriber](api/subscriber.md) - [Supported Message Types](api/message_types.md) + - [Service Server](api/service_server.md) + - [Service Client](api/service_client.md) - [Project Status](project_status.md) - [About](about.md) diff --git a/docs/src/api/service_client.md b/docs/src/api/service_client.md new file mode 100644 index 0000000..bce8de5 --- /dev/null +++ b/docs/src/api/service_client.md @@ -0,0 +1,51 @@ +# Service Client + +The `ServiceClient` API allows a Rust application to call eCAL services, either generically or per-instance. + +## Connecting to a Service + +```rust +use rustecal::service::client::ServiceClient; + +let client = ServiceClient::new("mirror")?; +``` + +## Calling Methods + +```rust +use rustecal::service::types::ServiceRequest; + +let request = ServiceRequest { + payload: b"stressed".to_vec(), +}; + +let response = client.call("echo", request, Some(1000)); +``` + +To call all connected instances: + +```rust +for instance in client.get_client_instances() { + let response = instance.call("reverse", request.clone(), Some(1000)); +} +``` + +## Return Handling + +```rust +match response { + Some(res) if res.success => { + println!("Response: {}", String::from_utf8_lossy(&res.payload)); + } + Some(res) => { + println!("Error: {}", res.error_msg.unwrap_or("Unknown error".into())); + } + None => { + println!("No response or timeout."); + } +} +``` + +## Runtime Compatibility + +This API matches the usage and behavior of `mirror_client.cpp` in the eCAL C++ samples. \ No newline at end of file diff --git a/docs/src/api/service_server.md b/docs/src/api/service_server.md new file mode 100644 index 0000000..a15c6b5 --- /dev/null +++ b/docs/src/api/service_server.md @@ -0,0 +1,50 @@ +# Service Server + +The `ServiceServer` API allows Rust applications to act as eCAL service providers using a simple, callback-based interface that mirrors the C++ and C APIs. + +## Registering Methods + +To provide services, create a `ServiceServer` and register one or more methods by name: + +```rust +use rustecal::service::server::ServiceServer; +use rustecal::service::types::MethodInfo; + +let mut server = ServiceServer::new("mirror")?; + +server.add_method("echo", Box::new(|_info: MethodInfo, request: &[u8]| { + request.to_vec() +}))?; + +server.add_method("reverse", Box::new(|_info, request| { + let mut reversed = request.to_vec(); + reversed.reverse(); + reversed +}))?; +``` + +## Method Signatures + +The callback signature follows: + +```rust +Fn(MethodInfo, &[u8]) -> Vec +``` + +This is safe, allocation-free on the input side, and flexible for any binary or textual payloads. + +## Example Output + +``` +Method : 'echo' called +Request : stressed +Response : stressed + +Method : 'reverse' called +Request : stressed +Response : desserts +``` + +## Runtime Compatibility + +This API is fully compatible with the C++ `mirror_server.cpp` and C `mirror_server.c` examples. \ No newline at end of file diff --git a/docs/src/project_status.md b/docs/src/project_status.md index 7d80f6d..494da9f 100644 --- a/docs/src/project_status.md +++ b/docs/src/project_status.md @@ -1,10 +1,11 @@ # Roadmap - [x] Cross-platform support (Windows, Linux) -- [x] Safe API for initialization, shutdown, and pub/sub -- [x] Typed pub/sub APIs +- [x] Safe API for initialization, shutdown +- [x] Binary publish/subscribe API +- [x] Typed publish/subscribe API - [x] Modular type crates (string, bytes, protobuf) -- [x] Examples for all supported types +- [x] Binary server/client API +- [x] Examples for all publish/subscribe and client/server - [ ] Protobuf descriptor introspection -- [ ] eCAL Services (RPC-style) - [ ] Monitoring and logging support diff --git a/rustecal-samples/pubsub/blob_receive/src/main.rs b/rustecal-samples/pubsub/blob_receive/src/main.rs index 848b7b1..d07b8e1 100644 --- a/rustecal-samples/pubsub/blob_receive/src/main.rs +++ b/rustecal-samples/pubsub/blob_receive/src/main.rs @@ -3,7 +3,7 @@ use rustecal_types_bytes::BytesMessage; use rustecal::pubsub::typed_subscriber::Received; fn main() { - Ecal::initialize(Some("bytes subscriber rust"), EcalComponents::DEFAULT) + Ecal::initialize(Some("blob receive rust"), EcalComponents::DEFAULT) .expect("eCAL initialization failed"); let mut subscriber = TypedSubscriber::::new("blob") diff --git a/rustecal-samples/pubsub/blob_send/src/main.rs b/rustecal-samples/pubsub/blob_send/src/main.rs index 3b2e416..5d394b4 100644 --- a/rustecal-samples/pubsub/blob_send/src/main.rs +++ b/rustecal-samples/pubsub/blob_send/src/main.rs @@ -2,7 +2,7 @@ use rustecal::{Ecal, EcalComponents, TypedPublisher}; use rustecal_types_bytes::BytesMessage; fn main() { - Ecal::initialize(Some("bytes publisher rust"), EcalComponents::DEFAULT) + Ecal::initialize(Some("blob send rust"), EcalComponents::DEFAULT) .expect("eCAL initialization failed"); let publisher = TypedPublisher::::new("blob") diff --git a/rustecal-samples/pubsub/hello_receive/src/main.rs b/rustecal-samples/pubsub/hello_receive/src/main.rs index 6ed6daa..21bd0b2 100644 --- a/rustecal-samples/pubsub/hello_receive/src/main.rs +++ b/rustecal-samples/pubsub/hello_receive/src/main.rs @@ -3,7 +3,7 @@ use rustecal_types_string::StringMessage; use rustecal::pubsub::typed_subscriber::Received; fn main() { - Ecal::initialize(Some("hello string subscriber rust"), EcalComponents::DEFAULT) + Ecal::initialize(Some("hello receive rust"), EcalComponents::DEFAULT) .expect("eCAL initialization failed"); let mut subscriber = TypedSubscriber::::new("hello") diff --git a/rustecal-samples/pubsub/hello_send/src/main.rs b/rustecal-samples/pubsub/hello_send/src/main.rs index f6932eb..02998de 100644 --- a/rustecal-samples/pubsub/hello_send/src/main.rs +++ b/rustecal-samples/pubsub/hello_send/src/main.rs @@ -2,7 +2,7 @@ use rustecal::{Ecal, EcalComponents, TypedPublisher}; use rustecal_types_string::StringMessage; fn main() { - Ecal::initialize(Some("hello string publisher rust"), EcalComponents::DEFAULT) + Ecal::initialize(Some("hello send rust"), EcalComponents::DEFAULT) .expect("eCAL initialization failed"); let publisher: TypedPublisher = TypedPublisher::::new("hello") diff --git a/rustecal-samples/pubsub/person_receive/src/main.rs b/rustecal-samples/pubsub/person_receive/src/main.rs index ea35b6b..50438a2 100644 --- a/rustecal-samples/pubsub/person_receive/src/main.rs +++ b/rustecal-samples/pubsub/person_receive/src/main.rs @@ -17,7 +17,7 @@ use people::Person; impl IsProtobufType for Person {} fn main() { - Ecal::initialize(Some("person protobuf subscriber rust"), EcalComponents::DEFAULT) + Ecal::initialize(Some("person receive rust"), EcalComponents::DEFAULT) .expect("eCAL initialization failed"); let mut subscriber = TypedSubscriber::>::new("person") diff --git a/rustecal-samples/pubsub/person_send/src/main.rs b/rustecal-samples/pubsub/person_send/src/main.rs index b536e00..957dc84 100644 --- a/rustecal-samples/pubsub/person_send/src/main.rs +++ b/rustecal-samples/pubsub/person_send/src/main.rs @@ -15,7 +15,7 @@ use people::Person; impl IsProtobufType for Person {} fn main() { - Ecal::initialize(Some("person protobuf publisher rust"), EcalComponents::DEFAULT) + Ecal::initialize(Some("person send rust"), EcalComponents::DEFAULT) .expect("eCAL initialization failed"); let publisher = TypedPublisher::>::new("person") diff --git a/rustecal-samples/service/mirror_client/.gitignore b/rustecal-samples/service/mirror_client/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/rustecal-samples/service/mirror_client/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rustecal-samples/service/mirror_client/Cargo.toml b/rustecal-samples/service/mirror_client/Cargo.toml new file mode 100644 index 0000000..3c44874 --- /dev/null +++ b/rustecal-samples/service/mirror_client/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "mirror_client" +version = "0.1.0" +edition = "2024" + +[dependencies] +rustecal = { path = "../../../rustecal" } diff --git a/rustecal-samples/service/mirror_client/src/main.rs b/rustecal-samples/service/mirror_client/src/main.rs new file mode 100644 index 0000000..5654811 --- /dev/null +++ b/rustecal-samples/service/mirror_client/src/main.rs @@ -0,0 +1,66 @@ +use rustecal::{Ecal, EcalComponents}; +use rustecal::service::client::ServiceClient; +use std::thread; +use std::time::Duration; + +fn main() -> Result<(), Box> { + // Initialize eCAL + Ecal::initialize(Some("mirror client rust"), EcalComponents::DEFAULT) + .expect("eCAL initialization failed"); + + let client = ServiceClient::new("mirror")?; + + // Wait until connected + while client.get_client_instances().is_empty() { + println!("Waiting for a service .."); + thread::sleep(Duration::from_secs(1)); + } + + let methods = ["echo", "reverse"]; + let mut i = 0; + + while Ecal::ok() { + let method_name = methods[i % methods.len()]; + i += 1; + + let request = rustecal::service::types::ServiceRequest { + payload: b"stressed".to_vec(), + }; + + for instance in client.get_client_instances() { + let response = instance.call(method_name, request.clone(), Some(1000)); + + println!(); + println!("Method '{}' called with message: stressed", method_name); + + match response { + Some(res) => { + match rustecal::service::types::CallState::from(res.success as i32) { + rustecal::service::types::CallState::Executed => { + let text = String::from_utf8_lossy(&res.payload); + println!( + "Received response: {} from service id {:?}", + text, res.server_id.service_id.entity_id + ); + } + rustecal::service::types::CallState::Failed => { + println!( + "Received error: {} from service id {:?}", + res.error_msg.unwrap_or_else(|| "Unknown".into()), + res.server_id.service_id.entity_id + ); + } + _ => {} + } + } + None => { + println!("Method blocking call failed .."); + } + } + } + + thread::sleep(Duration::from_secs(1)); + } + + Ok(()) +} diff --git a/rustecal-samples/service/mirror_client_instances/.gitignore b/rustecal-samples/service/mirror_client_instances/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/rustecal-samples/service/mirror_client_instances/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rustecal-samples/service/mirror_client_instances/Cargo.toml b/rustecal-samples/service/mirror_client_instances/Cargo.toml new file mode 100644 index 0000000..795bd1f --- /dev/null +++ b/rustecal-samples/service/mirror_client_instances/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "mirror_client_instances" +version = "0.1.0" +edition = "2024" + +[dependencies] +rustecal = { path = "../../../rustecal" } diff --git a/rustecal-samples/service/mirror_client_instances/src/main.rs b/rustecal-samples/service/mirror_client_instances/src/main.rs new file mode 100644 index 0000000..4f0fd56 --- /dev/null +++ b/rustecal-samples/service/mirror_client_instances/src/main.rs @@ -0,0 +1,65 @@ +use rustecal::{Ecal, EcalComponents}; +use rustecal::service::client::ServiceClient; +use rustecal::service::types::ServiceRequest; +use std::thread; +use std::time::Duration; + +fn main() -> Result<(), Box> { + // Initialize eCAL + Ecal::initialize(Some("mirror client instances rust"), EcalComponents::DEFAULT) + .expect("eCAL initialization failed"); + + let client = ServiceClient::new("mirror")?; + let methods = ["echo", "reverse"]; + let mut i = 0; + + while Ecal::ok() { + let method = methods[i % methods.len()]; + i += 1; + + let request = ServiceRequest { + payload: b"stressed".to_vec(), + }; + + let instances = client.get_client_instances(); + + if instances.is_empty() { + println!("No service instances available."); + } else { + for instance in instances { + let response = instance.call(method, request.clone(), Some(1000)); + + println!(); + println!( + "Method '{}' called with message: stressed", + method + ); + + match response { + Some(res) => { + if res.success { + println!( + "Received response: {} from service id {:?}", + String::from_utf8_lossy(&res.payload), + res.server_id.service_id.entity_id + ); + } else { + println!( + "Received error: {} from service id {:?}", + res.error_msg.unwrap_or_else(|| "Unknown".into()), + res.server_id.service_id.entity_id + ); + } + } + None => { + println!("Call failed or timed out."); + } + } + } + } + + thread::sleep(Duration::from_secs(1)); + } + + Ok(()) +} diff --git a/rustecal-samples/service/mirror_server/.gitignore b/rustecal-samples/service/mirror_server/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/rustecal-samples/service/mirror_server/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rustecal-samples/service/mirror_server/Cargo.toml b/rustecal-samples/service/mirror_server/Cargo.toml new file mode 100644 index 0000000..46c1af5 --- /dev/null +++ b/rustecal-samples/service/mirror_server/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "mirror_server" +version = "0.1.0" +edition = "2024" + +[dependencies] +rustecal = { path = "../../../rustecal" } diff --git a/rustecal-samples/service/mirror_server/src/main.rs b/rustecal-samples/service/mirror_server/src/main.rs new file mode 100644 index 0000000..5444e5b --- /dev/null +++ b/rustecal-samples/service/mirror_server/src/main.rs @@ -0,0 +1,38 @@ +use rustecal::{Ecal, EcalComponents}; +use rustecal::service::server::ServiceServer; +use rustecal::service::types::MethodInfo; + +fn main() -> Result<(), Box> { + // Initialize eCAL + Ecal::initialize(Some("mirror server rust"), EcalComponents::DEFAULT) + .expect("eCAL initialization failed"); + + // Create the service server named "mirror" + let mut server = ServiceServer::new("mirror")?; + + // Register "echo" method: respond with request unchanged + server.add_method("echo", Box::new(|info: MethodInfo, req: &[u8]| { + println!("Method : '{}' called", info.method_name); + println!("Request : {}", String::from_utf8_lossy(req)); + println!("Response : {}\n", String::from_utf8_lossy(req)); + req.to_vec() + }))?; + + // Register "reverse" method: respond with request reversed + server.add_method("reverse", Box::new(|info: MethodInfo, req: &[u8]| { + let mut reversed = req.to_vec(); + reversed.reverse(); + println!("Method : '{}' called", info.method_name); + println!("Request : {}", String::from_utf8_lossy(req)); + println!("Response : {}\n", String::from_utf8_lossy(&reversed)); + reversed + }))?; + + println!("Rust mirror service running. Press Ctrl+C to exit."); + + while Ecal::ok() { + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + Ok(()) +} diff --git a/rustecal-types-bytes/src/lib.rs b/rustecal-types-bytes/src/lib.rs index 9e8bd6f..a295959 100644 --- a/rustecal-types-bytes/src/lib.rs +++ b/rustecal-types-bytes/src/lib.rs @@ -5,7 +5,7 @@ //! enabling ergonomic and type-safe publishing and subscribing of binary blobs. use rustecal::{PublisherMessage, SubscriberMessage}; -use rustecal::pubsub::types::DataTypeInfo; +use rustecal::ecal::types::DataTypeInfo; /// A wrapper for raw binary data transmitted via eCAL. /// diff --git a/rustecal-types-protobuf/src/lib.rs b/rustecal-types-protobuf/src/lib.rs index 6d5ec13..7a50cf9 100644 --- a/rustecal-types-protobuf/src/lib.rs +++ b/rustecal-types-protobuf/src/lib.rs @@ -19,7 +19,7 @@ use prost::Message; use rustecal::pubsub::{PublisherMessage, SubscriberMessage}; -use rustecal::pubsub::types::DataTypeInfo; +use rustecal::ecal::types::DataTypeInfo; /// Marker trait to opt-in a Protobuf type for use with eCAL. /// diff --git a/rustecal-types-string/src/lib.rs b/rustecal-types-string/src/lib.rs index e84ffc1..04491c3 100644 --- a/rustecal-types-string/src/lib.rs +++ b/rustecal-types-string/src/lib.rs @@ -8,7 +8,7 @@ //! [`TypedPublisher`] and [`TypedSubscriber`] respectively. use rustecal::{PublisherMessage, SubscriberMessage}; -use rustecal::pubsub::types::DataTypeInfo; +use rustecal::ecal::types::DataTypeInfo; use std::str; /// A wrapper for UTF-8 string messages used with typed eCAL pub/sub. diff --git a/rustecal/Cargo.toml b/rustecal/Cargo.toml index 1d30def..02fe9a2 100644 --- a/rustecal/Cargo.toml +++ b/rustecal/Cargo.toml @@ -6,4 +6,5 @@ edition = "2024" [dependencies] rustecal-sys = { path = "../rustecal-sys" } +libc = "0.2" bitflags = "2.4" diff --git a/rustecal/src/ecal/mod.rs b/rustecal/src/ecal/mod.rs index 6517ad0..885a086 100644 --- a/rustecal/src/ecal/mod.rs +++ b/rustecal/src/ecal/mod.rs @@ -2,4 +2,6 @@ pub mod components; pub use components::EcalComponents; pub mod core; -pub use core::Ecal; \ No newline at end of file +pub use core::Ecal; + +pub mod types; \ No newline at end of file diff --git a/rustecal/src/ecal/types.rs b/rustecal/src/ecal/types.rs new file mode 100644 index 0000000..eba58e7 --- /dev/null +++ b/rustecal/src/ecal/types.rs @@ -0,0 +1,79 @@ +//! Common eCAL types shared across pubsub and service layers. + +use rustecal_sys::*; +use std::ffi::{CStr}; +use std::os::raw::c_char; + +/// Represents a globally unique entity in eCAL. +#[derive(Debug, Clone)] +pub struct EntityId { + pub entity_id: u64, + pub process_id: i32, + pub host_name: String, +} + +impl From for EntityId { + fn from(raw: eCAL_SEntityId) -> Self { + Self { + entity_id: raw.entity_id, + process_id: raw.process_id, + host_name: cstr_to_string(raw.host_name), + } + } +} + +/// Rust-safe representation of `eCAL_SDataTypeInformation`. +#[derive(Debug, Clone)] +pub struct DataTypeInfo { + pub type_name: String, + pub encoding: String, + pub descriptor: Vec, +} + +impl From for DataTypeInfo { + fn from(info: eCAL_SDataTypeInformation) -> Self { + let type_name = cstr_to_string(info.name); + let encoding = cstr_to_string(info.encoding); + let descriptor = if info.descriptor.is_null() || info.descriptor_length == 0 { + vec![] + } else { + unsafe { + std::slice::from_raw_parts(info.descriptor as *const u8, info.descriptor_length) + .to_vec() + } + }; + + Self { + type_name, + encoding, + descriptor, + } + } +} + +/// Rust-safe representation of `eCAL_SVersion`. +#[derive(Debug, Clone)] +pub struct Version { + pub major: i32, + pub minor: i32, + pub patch: i32, +} + +impl From for Version { + fn from(raw: eCAL_SVersion) -> Self { + Self { + major: raw.major, + minor: raw.minor, + patch: raw.patch, + } + } +} + +/// Helper to safely convert null-terminated C strings. +fn cstr_to_string(ptr: *const c_char) -> String { + if ptr.is_null() { + String::new() + } else { + unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() } + } +} diff --git a/rustecal/src/lib.rs b/rustecal/src/lib.rs index 29f3701..b54c8c7 100644 --- a/rustecal/src/lib.rs +++ b/rustecal/src/lib.rs @@ -42,4 +42,6 @@ pub use pubsub::{ // Optional if needed by demos: pub use pubsub::publisher::Publisher; -pub use pubsub::subscriber::Subscriber; \ No newline at end of file +pub use pubsub::subscriber::Subscriber; +// Service module +pub mod service; diff --git a/rustecal/src/pubsub/publisher.rs b/rustecal/src/pubsub/publisher.rs index b80d14c..cd6b4c8 100644 --- a/rustecal/src/pubsub/publisher.rs +++ b/rustecal/src/pubsub/publisher.rs @@ -1,5 +1,6 @@ -use crate::pubsub::types::{DataTypeInfo, TopicId}; use rustecal_sys::*; +use crate::ecal::types::DataTypeInfo; +use crate::pubsub::types::TopicId; use std::ffi::{CStr, CString}; use std::ptr; @@ -133,7 +134,7 @@ impl Publisher { if raw.is_null() { None } else { - Some(*(raw as *const TopicId)) + Some((*(raw as *const TopicId)).clone()) } } } diff --git a/rustecal/src/pubsub/subscriber.rs b/rustecal/src/pubsub/subscriber.rs index 3d275a0..047c201 100644 --- a/rustecal/src/pubsub/subscriber.rs +++ b/rustecal/src/pubsub/subscriber.rs @@ -1,5 +1,6 @@ use rustecal_sys::*; -use crate::pubsub::types::{DataTypeInfo, TopicId}; +use crate::ecal::types::DataTypeInfo; +use crate::pubsub::types::TopicId; use std::ffi::{CString, CStr, c_void}; use std::ptr; @@ -126,7 +127,7 @@ impl Subscriber { if raw.is_null() { None } else { - Some(*(raw as *const TopicId)) + Some((*(raw as *const TopicId)).clone()) } } } diff --git a/rustecal/src/pubsub/typed_publisher.rs b/rustecal/src/pubsub/typed_publisher.rs index 334118a..b1c2002 100644 --- a/rustecal/src/pubsub/typed_publisher.rs +++ b/rustecal/src/pubsub/typed_publisher.rs @@ -1,5 +1,6 @@ use crate::pubsub::publisher::Publisher; -use crate::pubsub::types::{DataTypeInfo, TopicId}; +use crate::ecal::types::DataTypeInfo; +use crate::pubsub::types::TopicId; use std::marker::PhantomData; /// Trait for types that can be published via [`TypedPublisher`]. diff --git a/rustecal/src/pubsub/typed_subscriber.rs b/rustecal/src/pubsub/typed_subscriber.rs index 3e665b0..e936c3d 100644 --- a/rustecal/src/pubsub/typed_subscriber.rs +++ b/rustecal/src/pubsub/typed_subscriber.rs @@ -1,5 +1,6 @@ use crate::pubsub::subscriber::Subscriber; -use crate::pubsub::types::{DataTypeInfo, TopicId}; +use crate::ecal::types::DataTypeInfo; +use crate::pubsub::types::TopicId; use rustecal_sys::{eCAL_SDataTypeInformation, eCAL_SReceiveCallbackData, eCAL_STopicId}; use std::ffi::{c_void, CStr}; use std::marker::PhantomData; diff --git a/rustecal/src/pubsub/types.rs b/rustecal/src/pubsub/types.rs index 8d976fe..9887181 100644 --- a/rustecal/src/pubsub/types.rs +++ b/rustecal/src/pubsub/types.rs @@ -1,164 +1,30 @@ -use std::ffi::c_void; -use std::os::raw::c_char; +//! Types used by the pub/sub layer of eCAL. -/// Rust-side representation of message metadata used in eCAL. -/// -/// This structure is used to describe the encoding, type name, and optional -/// schema (e.g. protobuf descriptor) associated with a message. -/// -/// It is passed to publishers and subscribers to ensure that the message -/// type and encoding are correctly interpreted across systems. -#[derive(Debug, Clone)] -pub struct DataTypeInfo { - /// Encoding format (e.g. "proto", "string", "raw"). - pub encoding: String, - - /// Logical or fully-qualified type name (e.g. `pb.MyType`). - pub type_name: String, - - /// Optional binary descriptor for the message schema (e.g. protobuf descriptor). - pub descriptor: Vec, -} +use crate::ecal::types::EntityId; +use rustecal_sys::*; +use std::ffi::CStr; -/// An identifier used by eCAL to distinguish topics. -/// -/// This includes the internal UUID-style ID and the topic name as a raw C string. -#[repr(C)] -#[derive(Debug, Copy, Clone)] +/// Internal eCAL topic identifier, used by publishers and subscribers. +#[derive(Debug, Clone)] pub struct TopicId { - /// Unique entity identifier assigned to the topic. - pub topic_id: EntityId, - - /// C string pointer to the topic name. - pub topic_name: *const c_char, -} - -/// A generic 128-bit UUID-style identifier used across eCAL entities. -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct EntityId { - /// Raw 128-bit identifier value. - pub id: [u8; 16], -} - -/// Enum of publisher events that may trigger callbacks. -/// -/// Events are reported through `PublisherEventCallbackData`. -#[repr(C)] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum PublisherEvent { - /// No event occurred. - None = 0, - - /// A subscriber has connected. - Connected = 1, - - /// A subscriber has disconnected. - Disconnected = 2, - - /// A message was dropped. - Dropped = 3, -} - -/// Data structure provided to publisher event callbacks. -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct PublisherEventCallbackData { - /// The type of event that occurred. - pub event: PublisherEvent, - - /// Timestamp of the event (microseconds). - pub time: i64, - - /// Logical clock value at time of event. - pub clock: i64, - - /// State code (implementation-specific). - pub state: i32, -} - -/// Enum of subscriber events that may trigger callbacks. -#[repr(C)] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum SubscriberEvent { - /// No event occurred. - None = 0, - - /// A publisher has connected. - Connected = 1, - - /// A publisher has disconnected. - Disconnected = 2, - - /// A message was dropped. - Dropped = 3, -} - -/// Data structure provided to subscriber event callbacks. -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct SubscriberEventCallbackData { - /// The type of event that occurred. - pub event: SubscriberEvent, - - /// Timestamp of the event (microseconds). - pub time: i64, - - /// Logical clock value at time of event. - pub clock: i64, - - /// State code (implementation-specific). - pub state: i32, -} - -/// Represents data received by a subscriber callback. -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct ReceiveCallbackData { - /// Pointer to the received message payload. - pub buffer: *const c_void, - - /// Size of the payload in bytes. - pub buffer_size: usize, - - /// Timestamp of when the message was sent (microseconds). - pub send_timestamp: i64, - - /// Clock value associated with the send event. - pub send_clock: i64, -} - -/// Raw FFI version of [`DataTypeInfo`] used in C interop. -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct FfiDataTypeInfo { - /// Pointer to encoding C string. - pub encoding: *const c_char, - - /// Pointer to type name C string. - pub name: *const c_char, - - /// Pointer to descriptor buffer. - pub descriptor: *const c_void, - - /// Length of the descriptor buffer. - pub descriptor_length: usize, -} - -/// Raw FFI version of [`TopicId`] used in callbacks. -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct FfiTopicId { - pub topic_id: EntityId, - pub topic_name: *const c_char, -} - -/// Raw FFI version of [`ReceiveCallbackData`] used in callbacks. -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct FfiReceiveCallbackData { - pub buffer: *const c_void, - pub buffer_size: usize, - pub send_timestamp: i64, - pub send_clock: i64, + pub entity_id: EntityId, + pub topic_name: String, +} + +impl From for TopicId { + fn from(raw: eCAL_STopicId) -> Self { + Self { + entity_id: EntityId::from(raw.topic_id), + topic_name: cstr_to_string(raw.topic_name), + } + } +} + +/// Helper function to safely convert a null-terminated C string. +fn cstr_to_string(ptr: *const std::os::raw::c_char) -> String { + if ptr.is_null() { + String::new() + } else { + unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() } + } } diff --git a/rustecal/src/service/client.rs b/rustecal/src/service/client.rs index e69de29..69bdbc0 100644 --- a/rustecal/src/service/client.rs +++ b/rustecal/src/service/client.rs @@ -0,0 +1,108 @@ +use crate::service::client_instance::ClientInstance; +use crate::service::types::{ServiceRequest}; +use crate::service::response::ServiceResponse; +use rustecal_sys::*; +use std::ffi::CString; +use std::os::raw::c_void; +use std::ptr; + +pub struct ServiceClient { + pub(crate) handle: *mut eCAL_ServiceClient, +} + +impl ServiceClient { + pub fn new(service_name: &str) -> Result { + let c_service = CString::new(service_name).map_err(|_| "Invalid service name")?; + let handle = unsafe { eCAL_ServiceClient_New(c_service.as_ptr(), ptr::null(), 0, None) }; + + if handle.is_null() { + Err("Failed to create eCAL_ServiceClient".into()) + } else { + Ok(Self { handle }) + } + } + + pub fn call(&self, method: &str, request: ServiceRequest, timeout_ms: Option) -> Option { + self.call_all(method, request, timeout_ms)?.pop() + } + + pub fn call_all( + &self, + method: &str, + request: ServiceRequest, + timeout_ms: Option, + ) -> Option> { + let c_method = CString::new(method).ok()?; + + let mut response_ptr: *mut eCAL_SServiceResponse = ptr::null_mut(); + let mut response_len: usize = 0; + + let timeout_ptr = timeout_ms + .as_ref() + .map(|t| t as *const i32) + .unwrap_or(ptr::null()); + + let result = unsafe { + eCAL_ServiceClient_CallWithResponse( + self.handle, + c_method.as_ptr(), + request.payload.as_ptr() as *const c_void, + request.payload.len(), + &mut response_ptr, + &mut response_len, + timeout_ptr, + ) + }; + + if result != 0 || response_ptr.is_null() || response_len == 0 { + return None; + } + + let mut responses = Vec::with_capacity(response_len); + + unsafe { + for i in 0..response_len { + let item = &*response_ptr.add(i); + responses.push(ServiceResponse::from_struct(item)); + } + + eCAL_Free(response_ptr as *mut c_void); + } + + Some(responses) + } + + pub fn get_client_instances(&self) -> Vec { + let mut result = Vec::new(); + + unsafe { + let list_ptr = eCAL_ServiceClient_GetClientInstances(self.handle); + if list_ptr.is_null() { + return result; + } + + let mut offset = 0; + loop { + let instance_ptr = *list_ptr.add(offset); + if instance_ptr.is_null() { + break; + } + + result.push(ClientInstance::from_raw(instance_ptr)); + offset += 1; + } + + eCAL_ClientInstances_Delete(list_ptr); + } + + result + } +} + +impl Drop for ServiceClient { + fn drop(&mut self) { + unsafe { + eCAL_ServiceClient_Delete(self.handle); + } + } +} diff --git a/rustecal/src/service/client_instance.rs b/rustecal/src/service/client_instance.rs new file mode 100644 index 0000000..abf1836 --- /dev/null +++ b/rustecal/src/service/client_instance.rs @@ -0,0 +1,57 @@ +use crate::service::types::{ServiceId, ServiceRequest}; +use crate::service::response::ServiceResponse; +use rustecal_sys::*; +use std::ffi::CString; +use std::os::raw::c_void; + +#[derive(Debug)] +pub struct ClientInstance { + pub(crate) instance: *mut eCAL_ClientInstance, +} + +impl ClientInstance { + pub fn from_raw(raw: *mut eCAL_ClientInstance) -> Self { + Self { instance: raw } + } + + pub fn call( + &self, + method: &str, + request: ServiceRequest, + timeout_ms: Option, + ) -> Option { + let c_method = CString::new(method).ok()?; + let timeout_ptr = timeout_ms + .as_ref() + .map(|t| t as *const i32) + .unwrap_or(std::ptr::null()); + + let response_ptr = unsafe { + eCAL_ClientInstance_CallWithResponse( + self.instance, + c_method.as_ptr(), + request.payload.as_ptr() as *const c_void, + request.payload.len(), + timeout_ptr, + ) + }; + + if response_ptr.is_null() { + return Some(ServiceResponse { + success: false, + server_id: ServiceId { + service_id: unsafe { std::mem::zeroed() }, + }, + error_msg: Some("call failed".into()), + payload: vec![], + }); + } + + unsafe { + let response = &*response_ptr; + let result = ServiceResponse::from_struct(response); + eCAL_Free(response_ptr as *mut c_void); + Some(result) + } + } +} diff --git a/rustecal/src/service/mod.rs b/rustecal/src/service/mod.rs index e69de29..32bdddb 100644 --- a/rustecal/src/service/mod.rs +++ b/rustecal/src/service/mod.rs @@ -0,0 +1,5 @@ +pub mod client; +pub mod client_instance; +pub mod server; +pub mod response; +pub mod types; diff --git a/rustecal/src/service/response.rs b/rustecal/src/service/response.rs new file mode 100644 index 0000000..7bb35cc --- /dev/null +++ b/rustecal/src/service/response.rs @@ -0,0 +1,49 @@ +use crate::service::types::{CallState, ServiceId}; +use rustecal_sys::*; +use std::ffi::CStr; + +/// Represents a structured response to a service request, +/// primarily used by clients to parse returned data. +#[derive(Debug, Clone)] +pub struct ServiceResponse { + pub success: bool, + pub server_id: ServiceId, + pub error_msg: Option, + pub payload: Vec, +} + +impl ServiceResponse { + /// Parses a raw FFI struct into a safe Rust response object. + pub fn from_struct(response: &eCAL_SServiceResponse) -> Self { + let success = CallState::from(response.call_state).is_success(); + + let server_id = unsafe { ServiceId::from_ffi(&response.server_id) }; + + let error_msg = if response.error_msg.is_null() { + None + } else { + Some(unsafe { + CStr::from_ptr(response.error_msg).to_string_lossy().into_owned() + }) + }; + + let payload = if response.response.is_null() || response.response_length == 0 { + vec![] + } else { + unsafe { + std::slice::from_raw_parts( + response.response as *const u8, + response.response_length, + ) + .to_vec() + } + }; + + Self { + success, + server_id, + error_msg, + payload, + } + } +} diff --git a/rustecal/src/service/server.rs b/rustecal/src/service/server.rs index e69de29..accd5b2 100644 --- a/rustecal/src/service/server.rs +++ b/rustecal/src/service/server.rs @@ -0,0 +1,127 @@ +use crate::service::types::{MethodInfo, ServiceCallback}; +use rustecal_sys::*; +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_int, c_void}; +use std::ptr; +use std::sync::{Arc, Mutex}; + +type SharedCallback = Arc>>; + +/// Represents a service server that can handle RPC-style requests. +pub struct ServiceServer { + handle: *mut eCAL_ServiceServer, + callbacks: SharedCallback, +} + +impl ServiceServer { + pub fn new(service_name: &str) -> Result { + let c_service_name = CString::new(service_name).map_err(|_| "Invalid service name")?; + + let callbacks: SharedCallback = Arc::new(Mutex::new(HashMap::new())); + let handle = unsafe { eCAL_ServiceServer_New(c_service_name.as_ptr(), None) }; + if handle.is_null() { + return Err("Failed to create eCAL_ServiceServer".into()); + } + + Ok(Self { + handle, + callbacks, + }) + } + + pub fn add_method(&mut self, method: &str, callback: ServiceCallback) -> Result<(), String> { + let c_method = CString::new(method).map_err(|_| "Invalid method name")?; + + let mut method_info: eCAL_SServiceMethodInformation = unsafe { std::mem::zeroed() }; + method_info.method_name = c_method.as_ptr(); + + self.callbacks + .lock() + .unwrap() + .insert(method.to_string(), callback); + + let result = unsafe { + eCAL_ServiceServer_SetMethodCallback( + self.handle, + &method_info, + Some(Self::dispatch), + Arc::as_ptr(&self.callbacks) as *mut c_void, + ) + }; + + if result != 0 { + Err("Failed to register method callback".into()) + } else { + Ok(()) + } + } + + unsafe extern "C" fn dispatch( + method_info: *const eCAL_SServiceMethodInformation, + request_ptr: *const c_void, + request_len: usize, + response_ptr: *mut *mut c_void, + response_len: *mut usize, + user_data: *mut c_void, + ) -> c_int { + let callbacks = { + let raw = user_data as *const Mutex>; + unsafe { &*raw }.lock().unwrap() + }; + + let method_name = { + if method_info.is_null() || unsafe { (*method_info).method_name }.is_null() { + return 1; + } + + let name_cstr = unsafe { CStr::from_ptr((*method_info).method_name) }; + match name_cstr.to_str() { + Ok(s) => s.to_string(), + Err(_) => return 1, + } + }; + + let request = if request_ptr.is_null() || request_len == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(request_ptr as *const u8, request_len) } + }; + + let info = MethodInfo { + method_name: method_name.clone(), + request_type: None, + response_type: None, + }; + + let cb = match callbacks.get(&method_name) { + Some(cb) => cb, + None => return 1, + }; + + let response = cb(info, request); + + let buffer = unsafe { eCAL_Malloc(response.len()) }; + if buffer.is_null() { + return 1; + } + + unsafe { + ptr::copy_nonoverlapping(response.as_ptr(), buffer as *mut u8, response.len()); + *response_ptr = buffer; + *response_len = response.len(); + } + + 0 + } +} + +impl Drop for ServiceServer { + fn drop(&mut self) { + unsafe { + eCAL_ServiceServer_Delete(self.handle); + let ptr = Arc::as_ptr(&self.callbacks) as *const _; + let _ = Arc::from_raw(ptr); + } + } +} diff --git a/rustecal/src/service/types.rs b/rustecal/src/service/types.rs new file mode 100644 index 0000000..72ebeef --- /dev/null +++ b/rustecal/src/service/types.rs @@ -0,0 +1,69 @@ +use rustecal_sys::*; + +#[derive(Debug, Clone, Copy)] +pub enum CallState { + None, + Executed, + Timeout, + Failed, + Unknown(i32), +} + +impl CallState { + pub fn is_success(&self) -> bool { + matches!(self, CallState::Executed) + } +} + +impl From for CallState { + fn from(value: i32) -> Self { + match value { + x if x == rustecal_sys::eCAL_eCallState_eCAL_eCallState_none => CallState::None, + x if x == rustecal_sys::eCAL_eCallState_eCAL_eCallState_executed => CallState::Executed, + x if x == rustecal_sys::eCAL_eCallState_eCAL_eCallState_timeouted => CallState::Timeout, + x if x == rustecal_sys::eCAL_eCallState_eCAL_eCallState_failed => CallState::Failed, + other => CallState::Unknown(other), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ServiceId { + pub service_id: eCAL_SEntityId, +} + +impl ServiceId { + pub unsafe fn from_ffi(raw: &eCAL_SServiceId) -> Self { + Self { + service_id: raw.service_id, + } + } +} + +#[derive(Debug, Clone)] +pub struct ServiceRequest { + pub payload: Vec, +} + +#[derive(Debug, Clone)] +pub struct ServiceResponse { + pub success: bool, + pub server_id: ServiceId, + pub error_msg: Option, + pub payload: Vec, +} + +/// Metadata passed to method callbacks about the method interface. +#[derive(Debug, Clone)] +pub struct MethodInfo { + pub method_name: String, + pub request_type: Option, + pub response_type: Option, +} + +/// The service callback signature used by ServiceServer. +/// +/// Mimics the eCAL C++ API: +/// - Accepts `MethodInfo` and a reference to request bytes +/// - Returns response bytes (`Vec`) +pub type ServiceCallback = Box Vec + Send + Sync + 'static>;