nros_macros/lib.rs
1//! Proc macros for nros message type generation
2//!
3//! Provides `#[derive(RosMessage)]` and `#[derive(RosService)]` macros.
4
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::{DeriveInput, Fields, LitByteStr, LitStr, Path, parse_macro_input};
8
9// Phase 212.N.9 — `nros::main!()` proc-macro family. Replaces today's
10// Entry-pkg `build.rs + include!(concat!(env!("OUT_DIR"), "/run_plan.rs"))`
11// shape with a one-line `main.rs`. See `main_macro.rs` for the impl.
12mod main_macro;
13
14/// One-line `fn main()` for Entry pkgs. Four forms:
15///
16/// ```ignore
17/// nros::main!(); // single-node self-bringup
18/// nros::main!(board = NativeBoard); // single-node, explicit board
19/// nros::main!(launch = "demo_bringup"); // multi-node, default launch
20/// nros::main!(launch = "demo_bringup:sim.launch.xml"); // multi-node, explicit file
21/// nros::main!(
22/// board = NativeBoard,
23/// launch = "demo_bringup:sim.launch.xml",
24/// args = [("use_sim", "true")],
25/// );
26/// ```
27///
28/// Reads `[package.metadata.nros.entry] deploy = "<board>"` from the
29/// Entry pkg's own `Cargo.toml` when `board = …` is absent; consults
30/// the workspace pkg-index (Phase 212.N.10) to resolve the bringup
31/// pkg's launch file (Phase 212.N.11 parser).
32///
33/// Emits `fn main()` that delegates to
34/// `<Board as ::nros::__macro_support::nros_platform::BoardEntry>::run(...)`,
35/// dispatching one `<pkg>::register(runtime)?;` call per
36/// launch-XML `<node>` entry. See `docs/design/0024-multi-node-workspace-layout.md`
37/// §11.6 for the design lock.
38#[proc_macro]
39pub fn main(input: TokenStream) -> TokenStream {
40 main_macro::expand(input)
41}
42
43/// Sanitise a cargo package name into a C-identifier-safe symbol component.
44///
45/// Cargo allows `-` in package names; C identifiers don't. Each non
46/// `[A-Za-z0-9_]` byte is replaced with `_` so the result is a valid
47/// suffix for the per-pkg register symbol emitted by [`node!`].
48///
49/// Crate-private (proc-macro crates can't export non-macro items); the
50/// `sanitize_tests` module exercises it directly.
51fn sanitize_pkg_name_for_symbol(pkg: &str) -> String {
52 let mut out = String::with_capacity(pkg.len());
53 for c in pkg.chars() {
54 if c.is_ascii_alphanumeric() || c == '_' {
55 out.push(c);
56 } else {
57 out.push('_');
58 }
59 }
60 out
61}
62
63/// Derive macro for ROS message types
64///
65/// Generates `Serialize`, `Deserialize`, and `RosMessage` implementations.
66///
67/// # Attributes
68///
69/// - `#[ros(type_name = "...")]` - Full ROS type name (required)
70/// - `#[ros(hash = "...")]` - RIHS type hash (required)
71///
72/// # Example
73///
74/// ```ignore
75/// use nros_macros::RosMessage;
76///
77/// #[derive(RosMessage)]
78/// #[ros(type_name = "std_msgs::msg::dds_::String_")]
79/// #[ros(hash = "abc123...")]
80/// pub struct StringMsg {
81/// pub data: heapless::String<256>,
82/// }
83/// ```
84#[proc_macro_derive(RosMessage, attributes(ros))]
85pub fn derive_ros_message(input: TokenStream) -> TokenStream {
86 let input = parse_macro_input!(input as DeriveInput);
87 let name = &input.ident;
88 let generics = &input.generics;
89 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
90
91 // Extract attributes
92 let mut type_name: Option<String> = None;
93 let mut type_hash: Option<String> = None;
94
95 for attr in &input.attrs {
96 if attr.path().is_ident("ros") {
97 let _ = attr.parse_nested_meta(|meta| {
98 if meta.path.is_ident("type_name") {
99 let value: LitStr = meta.value()?.parse()?;
100 type_name = Some(value.value());
101 } else if meta.path.is_ident("hash") {
102 let value: LitStr = meta.value()?.parse()?;
103 type_hash = Some(value.value());
104 }
105 Ok(())
106 });
107 }
108 }
109
110 let type_name = type_name.unwrap_or_else(|| format!("{}::msg::dds_::{}_", "unknown", name));
111 let type_hash = type_hash.unwrap_or_else(|| "0".repeat(64));
112
113 // Get fields
114 let fields = match &input.data {
115 syn::Data::Struct(data) => match &data.fields {
116 Fields::Named(fields) => &fields.named,
117 Fields::Unit => {
118 // Unit struct (no fields)
119 return generate_unit_struct_impl(
120 name,
121 &impl_generics,
122 &ty_generics,
123 where_clause,
124 &type_name,
125 &type_hash,
126 );
127 }
128 _ => {
129 return syn::Error::new_spanned(&input, "RosMessage only supports named fields")
130 .to_compile_error()
131 .into();
132 }
133 },
134 _ => {
135 return syn::Error::new_spanned(&input, "RosMessage can only be derived for structs")
136 .to_compile_error()
137 .into();
138 }
139 };
140
141 // Generate serialize calls for each field
142 let serialize_fields = fields.iter().map(|f| {
143 let field_name = &f.ident;
144 quote! {
145 self.#field_name.serialize(writer)?;
146 }
147 });
148
149 // Generate deserialize calls for each field
150 let deserialize_fields = fields.iter().map(|f| {
151 let field_name = &f.ident;
152 quote! {
153 #field_name: Deserialize::deserialize(reader)?,
154 }
155 });
156
157 let expanded = quote! {
158 impl #impl_generics nros_serdes::Serialize for #name #ty_generics #where_clause {
159 fn serialize(&self, writer: &mut nros_serdes::CdrWriter) -> Result<(), nros_serdes::SerError> {
160 use nros_serdes::Serialize;
161 #(#serialize_fields)*
162 Ok(())
163 }
164 }
165
166 impl #impl_generics nros_serdes::Deserialize for #name #ty_generics #where_clause {
167 fn deserialize(reader: &mut nros_serdes::CdrReader) -> Result<Self, nros_serdes::DeserError> {
168 use nros_serdes::Deserialize;
169 Ok(Self {
170 #(#deserialize_fields)*
171 })
172 }
173 }
174
175 impl #impl_generics nros_core::RosMessage for #name #ty_generics #where_clause {
176 const TYPE_NAME: &'static str = #type_name;
177 const TYPE_HASH: &'static str = #type_hash;
178 }
179 };
180
181 TokenStream::from(expanded)
182}
183
184/// Export a Rust type implementing `nros::Node` as the package node.
185///
186/// Phase 212.N.12 — `nros::node!()` is the canonical name (matches the
187/// rclcpp_components / ROS 2 launch.xml `<node pkg=…>` convention). The
188/// legacy `nros::node!()` macro and `Component*` trait family were
189/// retired in the same phase.
190///
191/// # Emitted items
192///
193/// Per invocation the macro currently emits:
194///
195/// 1. `pub fn register(runtime: &mut RuntimeCtx<'_>) -> Result<(), RuntimeError>`
196/// — the Entry-pkg-callable wrapper that registers the four typed
197/// `register` / `init` / `dispatch` / `tick` trampolines with the
198/// runtime by stable pkg name (Phase 212.N.7 step-3.4).
199/// 2. `#[unsafe(no_mangle)] pub extern "C" fn
200/// __nros_node_<pkg>_dispatch_strategy() -> u8` — Phase 216.A.5
201/// ABI export of the Node's [`DispatchStrategy`] discriminant
202/// (`<Type as Node>::DISPATCH as u8`). Read out-of-tree by
203/// `nros check` (216.D.1) and consumed from a separate compilation
204/// unit by the RTIC (216.B.3) / Embassy (216.C.3) dispatch tasks.
205/// `<pkg>` is the value of `CARGO_PKG_NAME` after
206/// `sanitize_pkg_name_for_symbol` (hyphens → underscores).
207///
208/// [`DispatchStrategy`]: ../nros/enum.DispatchStrategy.html
209///
210/// # Example
211///
212/// ```ignore
213/// struct Talker;
214///
215/// impl nros::Node for Talker {
216/// const NAME: &'static str = "talker";
217///
218/// fn register(ctx: &mut nros::NodeContext<'_>) -> nros::NodeResult<()> {
219/// let mut node = ctx.create_node(
220/// nros::NodeId::new("node"),
221/// nros::NodeOptions::new("talker"),
222/// )?;
223/// let _pub = node.create_publisher::<std_msgs::msg::String>(
224/// nros::EntityId::new("pub_chatter"),
225/// "chatter",
226/// )?;
227/// Ok(())
228/// }
229/// }
230///
231/// nros::node!(Talker);
232/// ```
233#[proc_macro]
234pub fn node(input: TokenStream) -> TokenStream {
235 node_impl(input)
236}
237
238fn node_impl(input: TokenStream) -> TokenStream {
239 let node_ty = parse_macro_input!(input as Path);
240
241 // Phase 212.N.7 step-3.4 — the package-name string handed to
242 // `register_dispatch_slot_dyn` (diagnostics) +
243 // `RuntimeError::ComponentRegister` is the sanitised pkg-name used by
244 // the codegen-emitted `run_plan` to reference each Node pkg, so
245 // the two strings round-trip 1:1.
246 //
247 // `proc_macro::tracked_env::var` is still unstable, so we use plain
248 // `std::env::var`. Cargo sets `CARGO_PKG_NAME` for every compilation
249 // (proc-macro crates inherit the parent crate's env at expansion).
250 // The fallback "unknown" only triggers in toolchains that don't set
251 // it (none today); it keeps the macro robust against future hosts.
252 let pkg_raw = std::env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "unknown".to_string());
253 let pkg_sym = sanitize_pkg_name_for_symbol(&pkg_raw);
254 let pkg_name_lit = pkg_sym.clone();
255
256 // Phase 216.A.5 — per-Node-pkg `DispatchStrategy` ABI export. The
257 // identifier is `__nros_node_<pkg>_dispatch_strategy`, sharing the
258 // same sanitised `<pkg>` substring as the diagnostics string above
259 // so out-of-tree tools (`nros check`, RTIC/Embassy dispatch tasks)
260 // can resolve the symbol from `CARGO_PKG_NAME` directly.
261 let dispatch_fn_name = quote::format_ident!("__nros_node_{}_dispatch_strategy", pkg_sym);
262 // Phase 257 (W0-B / D6) — the uniform cross-language install seam.
263 let component_install_fn_name = quote::format_ident!("__nros_component_{}_install", pkg_sym);
264 let component_present_name = quote::format_ident!("__NROS_NODE_PKG_{}_EXPORT_PRESENT", pkg_sym);
265 let component_class_name = quote::format_ident!("__nros_component_{}_class_name", pkg_sym);
266 let node_class_leaf = node_ty
267 .segments
268 .last()
269 .map(|segment| segment.ident.to_string())
270 .unwrap_or_else(|| "Node".to_string());
271 let component_class = format!("{}::{}\0", pkg_sym, node_class_leaf);
272 let component_class_len = component_class.len();
273 let component_class_bytes =
274 LitByteStr::new(component_class.as_bytes(), proc_macro2::Span::call_site());
275
276 // Phase 216.A.5 follow-up — the on_callback trampoline the B.3 RTIC /
277 // C.3 Embassy dispatch tasks invoke after dequeuing a
278 // `SignaledCallback<'static>` from the SPSC ring. The chosen signature
279 // is `extern "C"` with `(state, cb_id_ptr, cb_id_len, ctx)` because:
280 //
281 // 1. `SignaledCallback.cb_id` is `&'a str` (Phase 216.A.2 layout,
282 // `nros-platform/src/board/runtime.rs:47`) — a Rust fat pointer.
283 // The dispatch task already has `(ptr, len)` in hand from the
284 // `&str` it pulls off the ring, so passing them as separate
285 // `*const u8 + usize` args costs zero packing/unpacking.
286 // 2. Sibling export `__nros_node_<pkg>_dispatch_strategy` uses
287 // `extern "C"` — keeping the same ABI across the two
288 // macro-emitted symbols means out-of-tree tools (`nros check`,
289 // the RTIC/Embassy dispatch tasks themselves) resolve both via
290 // the same `dlsym` / objdump path with no cfg-driven ABI
291 // branching.
292 // 3. `extern "C"` with raw pointers is `no_std`-clean and matches
293 // the spec block in Phase 216.A.5 verbatim.
294 let on_callback_fn_name = quote::format_ident!("__nros_node_{}_on_callback", pkg_sym);
295
296 // Phase 212.N.7 step-6 — the legacy `#[unsafe(no_mangle)] extern
297 // "Rust" fn __nros_component_<pkg>_{register,init,dispatch,tick}`
298 // symbols and the `__NROS_COMPONENT_<PKG>_EXPORT_PRESENT` `#[used]`
299 // marker are gone. They existed for the Phase 212.M.5.a BSP baker
300 // (`freertos-qemu-mps2-an385-bsp` — retired in step-4), which
301 // walked the mangled names from a generated `system_main.rs`. The
302 // Phase 212.N Entry pkg path calls `<pkg>::register(runtime)`
303 // directly through the path API, so the four typed fns now live
304 // as local items inside the `register(runtime)` wrapper. The
305 // macro emits ONE public item: the wrapper itself.
306 let expanded = quote! {
307 // Phase 212.N.7 step-3.4 — Entry-pkg-callable `register(runtime)`
308 // wrapper. The codegen-emitted `run_plan(runtime)` body
309 // (`nros-build::generate_run_plan`) dispatches one
310 // `<pkg>::register(runtime)?` call per launch-XML `<node>` entry,
311 // so every Node pkg whose `lib.rs` invokes `nros::node!()`
312 // gets a stable per-pkg API here.
313 //
314 // SAFETY (transmutes below): typed `fn(args...) -> ret` /
315 // `unsafe fn(args...) -> ret` and the zero-arg
316 // `extern "Rust" fn()` aliases share the same ABI representation
317 // (one pointer); the transmute is purely a type-level
318 // reinterpretation. The impl-side transmute on the other side
319 // (`nros::node_runtime`) recovers the same typed signature
320 // before invoking — the round-trip is type-preserving so long
321 // as both sides agree on the typed signature, which they do
322 // (both live in `nros`).
323 //
324 // Phase 212.M-F.13 path (b): emit references go through
325 // `::nros::__macro_support::nros_platform::*` rather than the
326 // bare `::nros_platform::*` path so Node pkgs only need
327 // a single `nros` dep in their `Cargo.toml`. The
328 // `__macro_support` module is a `#[doc(hidden)]` re-export
329 // alias maintained by `packages/core/nros/src/lib.rs`.
330
331 // Phase 257 (W0-B / D6) — the uniform cross-language component-install seam.
332 // A typed Entry of ANY language calls
333 // `__nros_component_<pkg>_install(node, executor, self)` to install this Node
334 // onto the shared `Executor` it hands in (`executor` = the entry's
335 // `nros::global_handle()` / a node's `executor_handle()` = `*mut Executor`).
336 // For a Rust node `_node`/`_self` are unused — it self-creates its node (its
337 // `Node::NAME`) on the shared executor (phase-257 D7 Option C). Returns 0 on
338 // success, nonzero on a registration error (or -1 without the cffi runtime).
339 // `not_unsafe_ptr_arg_deref`: this is a C-ABI export — the raw-ptr
340 // params are mandatory (the foreign typed entry hands in the executor
341 // handle) and the body is `unsafe`. The deref-safety contract lives on
342 // `install_node_typed`'s caller (the entry), not this thin trampoline.
343 #[allow(clippy::not_unsafe_ptr_arg_deref)]
344 #[unsafe(no_mangle)]
345 pub extern "C" fn #component_install_fn_name(
346 _node: *const ::core::ffi::c_void,
347 executor: *mut ::core::ffi::c_void,
348 _self: *mut ::core::ffi::c_void,
349 ) -> i32 {
350 unsafe { ::nros::install_node_typed::<#node_ty>(executor) }
351 }
352
353 #[unsafe(no_mangle)]
354 pub static #component_present_name: u8 = 1;
355
356 #[unsafe(no_mangle)]
357 pub static #component_class_name: [u8; #component_class_len] = *#component_class_bytes;
358
359 #[unsafe(no_mangle)]
360 pub extern "C" fn #dispatch_fn_name() -> u8 {
361 <#node_ty as ::nros::Node>::DISPATCH as u8
362 }
363
364 // Phase 216.A.5 follow-up — extern "C" trampoline the B.3 RTIC
365 // and C.3 Embassy dispatch tasks call after dequeuing a
366 // `SignaledCallback<'static>` from the SPSC ring + resolving the
367 // owning Node-pkg by `CallbackId` (the registry is a separate
368 // follow-up). The call sequence on the dispatch-task side is:
369 //
370 // let cb = ring.pop()?; // SignaledCallback<'static>
371 // let pkg = lookup(cb.cb_id)?; // Node-by-cb_id registry (TBD)
372 // let f = dlsym(pkg, "__nros_node_<pkg>_on_callback")?;
373 // let state = node_state_table[pkg]; // *mut State, from i()
374 // f(state, cb.cb_id.as_ptr(), cb.cb_id.len(),
375 // cb.ctx_ptr);
376 //
377 // The trampoline reconstitutes the `&'static str` from
378 // `(cb_id_ptr, cb_id_len)` — both ends agree on UTF-8 (every
379 // `CallbackId` is built from a Rust `&str`), so the
380 // `from_utf8_unchecked` is sound by construction. The `state`
381 // and `ctx` re-cast mirrors the existing `unsafe fn d()` inside
382 // `register()` below — the only delta is the extern "C" surface
383 // + the (ptr, len) split for `cb_id`.
384 //
385 // SAFETY at the call site (documented for future
386 // dispatch-task authors — the trampoline does NOT recheck):
387 // * `state` must point to a live `<NodeTy as ExecutableNode>::State`
388 // produced by this pkg's `i()` (= same as the existing
389 // `unsafe fn d()` contract).
390 // * `cb_id_ptr` must point to `cb_id_len` valid UTF-8 bytes
391 // with `'static` lifetime — produced by the codegen-emitted
392 // `CallbackId(&'static str)` literals.
393 // * `ctx` must point to a live `CallbackCtx<'static>` the
394 // dispatch task owns for the duration of the call.
395 // * Concurrent calls against the same `state` are forbidden
396 // (same dispatch-slot non-reentrancy as `unsafe fn d()`).
397 #[unsafe(no_mangle)]
398 pub unsafe extern "C" fn #on_callback_fn_name(
399 state: *mut ::core::ffi::c_void,
400 cb_id_ptr: *const u8,
401 cb_id_len: usize,
402 ctx: *mut ::core::ffi::c_void,
403 ) {
404 // SAFETY: caller upholds the four bullets above.
405 let cb_id = unsafe {
406 let bytes = ::core::slice::from_raw_parts(cb_id_ptr, cb_id_len);
407 let s: &'static str = ::core::str::from_utf8_unchecked(bytes);
408 ::nros::CallbackId(s)
409 };
410 let state_mut = unsafe {
411 &mut *(state as *mut <#node_ty as ::nros::ExecutableNode>::State)
412 };
413 let ctx_mut = unsafe {
414 &mut *(ctx as *mut ::nros::CallbackCtx<'static>)
415 };
416 <#node_ty as ::nros::ExecutableNode>::on_callback(
417 state_mut,
418 ::nros::Callback::__from_id(cb_id),
419 ctx_mut,
420 );
421 }
422
423 /// Phase 216 final wave — framework-side per-Node registration.
424 ///
425 /// The `nros::main!()` proc-macro emits one
426 /// `<pkg>::register_dispatch(&mut executor)?;` call per
427 /// declared Node pkg in the RTIC / Embassy `#[init]` body
428 /// (see `packages/core/nros-macros/src/main_macro.rs`
429 /// `Framework::Rtic` / `Framework::Embassy` emit branches).
430 /// The call:
431 ///
432 /// 1. Constructs the Node's per-pkg `State` blob via
433 /// `<NodeTy as ExecutableNode>::init()`.
434 /// 2. Leaks the state into a raw `*mut c_void` pointer via
435 /// `__private_node_state_into_raw` (the same shape the
436 /// `register(runtime)` wrapper's local `i()` fn uses).
437 /// 3. Pushes `(state_ptr, __nros_node_<pkg>_on_callback)`
438 /// onto the `Executor`'s dispatch-slot registry, where
439 /// the framework dispatch task's
440 /// `executor.dispatch_callback(cb_id, ctx)` calls scan
441 /// and invoke it.
442 ///
443 /// Returns `Err(())` if the Executor's slot table is full
444 /// (`MAX_NODES` entries — raise via
445 /// `NROS_EXECUTOR_MAX_NODES` at build time). The Entry pkg
446 /// surfaces the failure with `expect("register dispatch")`
447 /// so a too-many-Nodes misconfig fails loud at boot rather
448 /// than dropping dispatch silently.
449 ///
450 /// References `::nros::Executor` which is gated on `rmw-cffi`
451 /// inside the `nros` crate. The Node pkg must depend on `nros`
452 /// with the `rmw-cffi` feature enabled (every shipped 216
453 /// example pkg does). A consumer w/o `rmw-cffi` enabled gets a
454 /// hard `cannot find type Executor` error at expansion site —
455 /// surfaces the misconfig loud rather than silently producing
456 /// a Node pkg that can't be deployed to a framework target.
457 pub fn register_dispatch(
458 executor: &mut ::nros::Executor,
459 ) -> ::core::result::Result<(), ()> {
460 let state: <#node_ty as ::nros::ExecutableNode>::State =
461 <#node_ty as ::nros::ExecutableNode>::init();
462 let state_ptr =
463 ::nros::__private_node_state_into_raw::<#node_ty>(state)
464 as *mut ::core::ffi::c_void;
465 executor.register_dispatch_slot(state_ptr, #on_callback_fn_name)
466 }
467
468 /// Phase 258 (Track 2, 2a) — owned-spin entry registration. The
469 /// codegen-emitted `run_plan(runtime)` body + `nros::main!`
470 /// owned-spin loop call `<pkg>::register(runtime)?` once per Node.
471 ///
472 /// This now installs through the **uniform `install` seam** — the
473 /// same `nros::install_node_typed` path the C / C++ typed entries
474 /// use — via a raw `*mut Executor` the runtime sink exposes
475 /// (`executor_handle()`), replacing the legacy opaque four-fn-ptr
476 /// `register_dispatch_slot_dyn` bridge. The installed component
477 /// enrolls in the executor's tick registry, so service-client /
478 /// action poll nodes tick (phase-257 D2 closed for owned-spin too).
479 ///
480 /// References `::nros::install_node_typed` → `::nros::Executor`,
481 /// gated on `rmw-cffi` inside `nros`; the Node pkg must depend on
482 /// `nros` with `rmw-cffi` (every owned-spin example pkg does). A
483 /// null handle (a sink with no live executor) maps to
484 /// `RuntimeError::NodeRegister`.
485 pub fn register(
486 runtime: &mut ::nros::__macro_support::nros_platform::RuntimeCtx<'_>,
487 ) -> ::core::result::Result<(), ::nros::__macro_support::nros_platform::RuntimeError> {
488 let executor = runtime.runtime.executor_handle();
489 // SAFETY: `executor` is the runtime sink's live `*mut Executor`
490 // (or null, which `install_node_typed` rejects as an error).
491 match unsafe { ::nros::install_node_typed::<#node_ty>(executor) } {
492 0 => ::core::result::Result::Ok(()),
493 _ => ::core::result::Result::Err(
494 ::nros::__macro_support::nros_platform::RuntimeError::NodeRegister(#pkg_name_lit),
495 ),
496 }
497 }
498 };
499
500 TokenStream::from(expanded)
501}
502
503fn generate_unit_struct_impl(
504 name: &syn::Ident,
505 impl_generics: &syn::ImplGenerics,
506 ty_generics: &syn::TypeGenerics,
507 where_clause: Option<&syn::WhereClause>,
508 type_name: &str,
509 type_hash: &str,
510) -> TokenStream {
511 let expanded = quote! {
512 impl #impl_generics nros_serdes::Serialize for #name #ty_generics #where_clause {
513 fn serialize(&self, _writer: &mut nros_serdes::CdrWriter) -> Result<(), nros_serdes::SerError> {
514 Ok(())
515 }
516 }
517
518 impl #impl_generics nros_serdes::Deserialize for #name #ty_generics #where_clause {
519 fn deserialize(_reader: &mut nros_serdes::CdrReader) -> Result<Self, nros_serdes::DeserError> {
520 Ok(Self {})
521 }
522 }
523
524 impl #impl_generics nros_core::RosMessage for #name #ty_generics #where_clause {
525 const TYPE_NAME: &'static str = #type_name;
526 const TYPE_HASH: &'static str = #type_hash;
527 }
528 };
529 TokenStream::from(expanded)
530}
531
532/// Derive macro for ROS service types
533///
534/// # Attributes
535///
536/// - `#[ros(service_name = "...")]` - Full ROS service name (required)
537/// - `#[ros(hash = "...")]` - RIHS service hash (required)
538/// - `#[ros(request = "RequestType")]` - Request type name (required)
539/// - `#[ros(reply = "ReplyType")]` - Reply type name (required)
540///
541/// # Example
542///
543/// ```ignore
544/// use nros_macros::RosService;
545///
546/// #[derive(RosService)]
547/// #[ros(service_name = "std_srvs::srv::dds_::Empty_")]
548/// #[ros(hash = "abc123...")]
549/// #[ros(request = "EmptyRequest")]
550/// #[ros(reply = "EmptyReply")]
551/// pub struct Empty;
552/// ```
553#[proc_macro_derive(RosService, attributes(ros))]
554pub fn derive_ros_service(input: TokenStream) -> TokenStream {
555 let input = parse_macro_input!(input as DeriveInput);
556 let name = &input.ident;
557
558 // Extract attributes
559 let mut service_name: Option<String> = None;
560 let mut service_hash: Option<String> = None;
561 let mut request_type: Option<syn::Ident> = None;
562 let mut reply_type: Option<syn::Ident> = None;
563
564 for attr in &input.attrs {
565 if attr.path().is_ident("ros") {
566 let _ = attr.parse_nested_meta(|meta| {
567 if meta.path.is_ident("service_name") {
568 let value: LitStr = meta.value()?.parse()?;
569 service_name = Some(value.value());
570 } else if meta.path.is_ident("hash") {
571 let value: LitStr = meta.value()?.parse()?;
572 service_hash = Some(value.value());
573 } else if meta.path.is_ident("request") {
574 let value: LitStr = meta.value()?.parse()?;
575 request_type = Some(syn::Ident::new(
576 &value.value(),
577 proc_macro2::Span::call_site(),
578 ));
579 } else if meta.path.is_ident("reply") {
580 let value: LitStr = meta.value()?.parse()?;
581 reply_type = Some(syn::Ident::new(
582 &value.value(),
583 proc_macro2::Span::call_site(),
584 ));
585 }
586 Ok(())
587 });
588 }
589 }
590
591 let service_name =
592 service_name.unwrap_or_else(|| format!("{}::srv::dds_::{}_", "unknown", name));
593 let service_hash = service_hash.unwrap_or_else(|| "0".repeat(64));
594
595 let request_type = match request_type {
596 Some(t) => t,
597 None => {
598 return syn::Error::new_spanned(
599 &input,
600 "RosService requires #[ros(request = \"...\")]",
601 )
602 .to_compile_error()
603 .into();
604 }
605 };
606
607 let reply_type = match reply_type {
608 Some(t) => t,
609 None => {
610 return syn::Error::new_spanned(&input, "RosService requires #[ros(reply = \"...\")]")
611 .to_compile_error()
612 .into();
613 }
614 };
615
616 let expanded = quote! {
617 impl nros_core::RosService for #name {
618 type Request = #request_type;
619 type Reply = #reply_type;
620
621 const SERVICE_NAME: &'static str = #service_name;
622 const SERVICE_HASH: &'static str = #service_hash;
623 }
624 };
625
626 TokenStream::from(expanded)
627}
628
629#[cfg(test)]
630mod sanitize_tests {
631 use super::sanitize_pkg_name_for_symbol;
632
633 #[test]
634 fn plain_pkg_name_is_passthrough() {
635 assert_eq!(sanitize_pkg_name_for_symbol("talker_pkg"), "talker_pkg");
636 }
637
638 #[test]
639 fn hyphens_become_underscores() {
640 assert_eq!(sanitize_pkg_name_for_symbol("my-cool-pkg"), "my_cool_pkg");
641 }
642
643 #[test]
644 fn mixed_specials_become_underscores() {
645 assert_eq!(sanitize_pkg_name_for_symbol("a.b+c-d"), "a_b_c_d");
646 }
647
648 #[test]
649 fn empty_is_empty() {
650 assert_eq!(sanitize_pkg_name_for_symbol(""), "");
651 }
652}