pamoja_ffi/routing.rs
1//! The C ABI for cost-aware mesh routing.
2//!
3//! These functions wrap [`pamoja_routing`] for callers that reach the SDK through
4//! the flat C boundary: a table that learns the way to a node from the traffic it
5//! already hears, and the per-packet decision of whether to deliver, relay, or
6//! fall back to flooding.
7//!
8//! A router holds state across calls, so it crosses as an opaque handle. Its table
9//! is sized when it is built rather than by the const generic the Rust crate uses,
10//! since a const generic cannot cross a C ABI at all;
11//! [`PAMOJA_ROUTING_DEFAULT_CAPACITY`] is what a caller with no reason to choose
12//! should pass.
13
14use pamoja_routing::{DynamicRouter, Forward};
15
16/// A reasonable routing table size for a caller with no reason to choose one.
17pub const PAMOJA_ROUTING_DEFAULT_CAPACITY: usize = 64;
18
19/// An opaque handle to one node routing table.
20///
21/// Create it with [`pamoja_router_new`], teach it with
22/// [`pamoja_router_observe`], and release it with [`pamoja_router_free`].
23pub struct PamojaRouter {
24 router: DynamicRouter,
25}
26
27/// A learned way to reach one node.
28///
29/// Every field is a scalar, so this crosses the boundary by value.
30#[repr(C)]
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct PamojaRoute {
33 /// The node this route reaches.
34 pub dst: u32,
35 /// The neighbor to send a packet to on the way there.
36 pub next_hop: u32,
37 /// What the route costs, usually in hops.
38 pub cost: u16,
39}
40
41/// What to do with a packet bound for a given node.
42#[repr(C)]
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum PamojaForward {
45 /// The packet is for this node; hand it to the application.
46 Deliver = 0,
47 /// A route is known; unicast the packet to the next hop reported alongside.
48 Relay = 1,
49 /// No route is known; fall back to flooding the packet.
50 Flood = 2,
51}
52
53/// Creates an empty routing table for a node.
54///
55/// # Arguments
56///
57/// * `address` - the address of this node, which is what
58/// [`pamoja_router_forward`] recognizes as a local delivery.
59/// * `capacity` - how many routes to make room for; pass
60/// [`PAMOJA_ROUTING_DEFAULT_CAPACITY`] when there is no reason to choose. A
61/// capacity of zero is allowed and makes every unknown destination flood.
62///
63/// # Returns
64///
65/// A handle the caller must release with [`pamoja_router_free`].
66#[no_mangle]
67pub extern "C" fn pamoja_router_new(address: u32, capacity: usize) -> *mut PamojaRouter {
68 Box::into_raw(Box::new(PamojaRouter {
69 router: DynamicRouter::new(address, capacity),
70 }))
71}
72
73/// Returns the address a router answers for.
74///
75/// # Returns
76///
77/// The node address, or 0 if `router` is null.
78///
79/// # Safety
80///
81/// `router` must be a live handle from [`pamoja_router_new`], or null.
82#[no_mangle]
83pub unsafe extern "C" fn pamoja_router_address(router: *const PamojaRouter) -> u32 {
84 if router.is_null() {
85 return 0;
86 }
87 (*router).router.address()
88}
89
90/// Learns a route from a packet that arrived.
91///
92/// When a packet from a distant node comes in via a neighbor, that neighbor is
93/// the way back to it. The table keeps the cheapest way it knows to each node, and
94/// when full gives up the most expensive route to make room for a cheaper one.
95///
96/// # Arguments
97///
98/// * `router` - the routing table.
99/// * `origin` - the node the packet came from.
100/// * `via` - the neighbor it arrived through.
101/// * `cost` - what that path costs, usually a hop count.
102///
103/// # Returns
104///
105/// `true` if the table changed, or `false` if it already knew a route at least
106/// this cheap, had no room for one this expensive, or `router` is null.
107///
108/// # Safety
109///
110/// `router` must be a live handle from [`pamoja_router_new`], or null.
111#[no_mangle]
112pub unsafe extern "C" fn pamoja_router_observe(
113 router: *mut PamojaRouter,
114 origin: u32,
115 via: u32,
116 cost: u16,
117) -> bool {
118 if router.is_null() {
119 return false;
120 }
121 (*router).router.observe(origin, via, cost)
122}
123
124/// Returns the neighbor to send a packet to on the way to a node.
125///
126/// # Arguments
127///
128/// * `router` - the routing table.
129/// * `dst` - the node to reach.
130/// * `out_next_hop` - receives the neighbor address.
131///
132/// # Returns
133///
134/// `true` when a route is known, with `*out_next_hop` written, or `false`
135/// otherwise.
136///
137/// # Safety
138///
139/// `router` must be a live handle from [`pamoja_router_new`], or null, and
140/// `out_next_hop` must point to a writable `uint32_t`.
141#[no_mangle]
142pub unsafe extern "C" fn pamoja_router_next_hop(
143 router: *const PamojaRouter,
144 dst: u32,
145 out_next_hop: *mut u32,
146) -> bool {
147 if router.is_null() || out_next_hop.is_null() {
148 return false;
149 }
150 match (*router).router.next_hop(dst) {
151 Some(next_hop) => {
152 *out_next_hop = next_hop;
153 true
154 }
155 None => false,
156 }
157}
158
159/// Returns what the known route to a node costs.
160///
161/// # Arguments
162///
163/// * `router` - the routing table.
164/// * `dst` - the node to reach.
165/// * `out_cost` - receives the cost.
166///
167/// # Returns
168///
169/// `true` when a route is known, with `*out_cost` written, or `false` otherwise.
170///
171/// # Safety
172///
173/// `router` must be a live handle from [`pamoja_router_new`], or null, and
174/// `out_cost` must point to a writable `uint16_t`.
175#[no_mangle]
176pub unsafe extern "C" fn pamoja_router_cost(
177 router: *const PamojaRouter,
178 dst: u32,
179 out_cost: *mut u16,
180) -> bool {
181 if router.is_null() || out_cost.is_null() {
182 return false;
183 }
184 match (*router).router.cost(dst) {
185 Some(cost) => {
186 *out_cost = cost;
187 true
188 }
189 None => false,
190 }
191}
192
193/// Returns the whole route to a node.
194///
195/// # Arguments
196///
197/// * `router` - the routing table.
198/// * `dst` - the node to reach.
199/// * `out_route` - receives the route.
200///
201/// # Returns
202///
203/// `true` when a route is known, with `*out_route` filled in, or `false`
204/// otherwise.
205///
206/// # Safety
207///
208/// `router` must be a live handle from [`pamoja_router_new`], or null, and
209/// `out_route` must point to a writable [`PamojaRoute`].
210#[no_mangle]
211pub unsafe extern "C" fn pamoja_router_route(
212 router: *const PamojaRouter,
213 dst: u32,
214 out_route: *mut PamojaRoute,
215) -> bool {
216 if router.is_null() || out_route.is_null() {
217 return false;
218 }
219 match (*router).router.route(dst) {
220 Some(route) => {
221 *out_route = PamojaRoute {
222 dst: route.dst(),
223 next_hop: route.next_hop(),
224 cost: route.cost(),
225 };
226 true
227 }
228 None => false,
229 }
230}
231
232/// Returns one of the routes a table holds, by its position among them.
233///
234/// Positions count the routes held from zero, in the order the table holds them, so
235/// reading positions `0` to [`pamoja_router_len`] minus one lists the whole table.
236///
237/// # Arguments
238///
239/// * `router` - the routing table.
240/// * `index` - the position of the route, counting from zero.
241/// * `out_route` - receives the route.
242///
243/// # Returns
244///
245/// `true` with `*out_route` filled in, or `false` when the table holds no route at
246/// that position.
247///
248/// # Safety
249///
250/// `router` must be a live handle from [`pamoja_router_new`], or null, and
251/// `out_route` must point to a writable [`PamojaRoute`].
252#[no_mangle]
253pub unsafe extern "C" fn pamoja_router_route_at(
254 router: *const PamojaRouter,
255 index: usize,
256 out_route: *mut PamojaRoute,
257) -> bool {
258 if router.is_null() || out_route.is_null() {
259 return false;
260 }
261 match (*router).router.routes().nth(index) {
262 Some(route) => {
263 *out_route = PamojaRoute {
264 dst: route.dst(),
265 next_hop: route.next_hop(),
266 cost: route.cost(),
267 };
268 true
269 }
270 None => false,
271 }
272}
273
274/// Decides what to do with a packet bound for a node.
275///
276/// # Arguments
277///
278/// * `router` - the routing table.
279/// * `dst` - the node the packet is addressed to.
280/// * `out_next_hop` - receives the neighbor to unicast to, written only when the
281/// answer is [`PamojaForward::Relay`].
282///
283/// # Returns
284///
285/// [`PamojaForward::Deliver`] when the packet is for this node,
286/// [`PamojaForward::Relay`] when a route is known, or [`PamojaForward::Flood`]
287/// when none is, which hands the packet back to the flooding layer. A null router
288/// answers [`PamojaForward::Flood`], the choice that always works.
289///
290/// # Safety
291///
292/// `router` must be a live handle from [`pamoja_router_new`], or null, and
293/// `out_next_hop` must point to a writable `uint32_t` or be null.
294#[no_mangle]
295pub unsafe extern "C" fn pamoja_router_forward(
296 router: *const PamojaRouter,
297 dst: u32,
298 out_next_hop: *mut u32,
299) -> PamojaForward {
300 if !out_next_hop.is_null() {
301 *out_next_hop = 0;
302 }
303 if router.is_null() {
304 return PamojaForward::Flood;
305 }
306 match (*router).router.forward(dst) {
307 Forward::Deliver => PamojaForward::Deliver,
308 Forward::Relay(next_hop) => {
309 if !out_next_hop.is_null() {
310 *out_next_hop = next_hop;
311 }
312 PamojaForward::Relay
313 }
314 Forward::Flood => PamojaForward::Flood,
315 }
316}
317
318/// Forgets the route to a node, for example after it stops answering.
319///
320/// # Arguments
321///
322/// * `router` - the routing table.
323/// * `dst` - the node to forget.
324///
325/// # Safety
326///
327/// `router` must be a live handle from [`pamoja_router_new`], or null.
328#[no_mangle]
329pub unsafe extern "C" fn pamoja_router_forget(router: *mut PamojaRouter, dst: u32) {
330 if router.is_null() {
331 return;
332 }
333 (*router).router.forget(dst);
334}
335
336/// Returns how many routes a table currently holds.
337///
338/// # Returns
339///
340/// The number of routes, or 0 if `router` is null.
341///
342/// # Safety
343///
344/// `router` must be a live handle from [`pamoja_router_new`], or null.
345#[no_mangle]
346pub unsafe extern "C" fn pamoja_router_len(router: *const PamojaRouter) -> usize {
347 if router.is_null() {
348 return 0;
349 }
350 (*router).router.len()
351}
352
353/// Returns how many routes a table can hold.
354///
355/// # Returns
356///
357/// The capacity it was created with, or 0 if `router` is null.
358///
359/// # Safety
360///
361/// `router` must be a live handle from [`pamoja_router_new`], or null.
362#[no_mangle]
363pub unsafe extern "C" fn pamoja_router_capacity(router: *const PamojaRouter) -> usize {
364 if router.is_null() {
365 return 0;
366 }
367 (*router).router.capacity()
368}
369
370/// Releases a routing table handle.
371///
372/// Passing null is a no-op.
373///
374/// # Safety
375///
376/// `router` must be a handle from [`pamoja_router_new`] that has not already been
377/// freed, or null. After this call it must not be used again.
378#[no_mangle]
379pub unsafe extern "C" fn pamoja_router_free(router: *mut PamojaRouter) {
380 if !router.is_null() {
381 drop(Box::from_raw(router));
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use std::ptr;
388
389 use super::*;
390
391 #[test]
392 fn a_cheaper_neighbor_replaces_the_route() {
393 let router = pamoja_router_new(0x01, PAMOJA_ROUTING_DEFAULT_CAPACITY);
394 // Safety: the handle was just created and the out-pointers are valid.
395 unsafe {
396 assert_eq!(pamoja_router_address(router), 0x01);
397 assert!(pamoja_router_observe(router, 0x09, 0x05, 2));
398
399 let mut next_hop = 0u32;
400 assert_eq!(
401 pamoja_router_forward(router, 0x09, &mut next_hop),
402 PamojaForward::Relay
403 );
404 assert_eq!(next_hop, 0x05);
405
406 assert!(pamoja_router_observe(router, 0x09, 0x07, 1));
407 let mut route = PamojaRoute {
408 dst: 0,
409 next_hop: 0,
410 cost: 0,
411 };
412 assert!(pamoja_router_route(router, 0x09, &mut route));
413 assert_eq!(
414 route,
415 PamojaRoute {
416 dst: 0x09,
417 next_hop: 0x07,
418 cost: 1
419 }
420 );
421 assert_eq!(pamoja_router_len(router), 1);
422 pamoja_router_free(router);
423 }
424 }
425
426 #[test]
427 fn a_packet_for_this_node_is_delivered_and_an_unknown_one_floods() {
428 let router = pamoja_router_new(0x01, PAMOJA_ROUTING_DEFAULT_CAPACITY);
429 // Safety: the handle was just created and the out-pointer is valid.
430 unsafe {
431 let mut next_hop = 0xFFFF_FFFFu32;
432 assert_eq!(
433 pamoja_router_forward(router, 0x01, &mut next_hop),
434 PamojaForward::Deliver
435 );
436 assert_eq!(next_hop, 0, "no next hop belongs to a local delivery");
437 assert_eq!(
438 pamoja_router_forward(router, 0x20, &mut next_hop),
439 PamojaForward::Flood
440 );
441 pamoja_router_free(router);
442 }
443 }
444
445 #[test]
446 fn a_forgotten_route_floods_again() {
447 let router = pamoja_router_new(0x01, PAMOJA_ROUTING_DEFAULT_CAPACITY);
448 // Safety: the handle was just created and the out-pointers are valid.
449 unsafe {
450 pamoja_router_observe(router, 0x09, 0x05, 2);
451 let mut cost = 0u16;
452 assert!(pamoja_router_cost(router, 0x09, &mut cost));
453 assert_eq!(cost, 2);
454
455 pamoja_router_forget(router, 0x09);
456 assert_eq!(pamoja_router_len(router), 0);
457 let mut next_hop = 0u32;
458 assert!(!pamoja_router_next_hop(router, 0x09, &mut next_hop));
459 assert_eq!(
460 pamoja_router_forward(router, 0x09, ptr::null_mut()),
461 PamojaForward::Flood
462 );
463 pamoja_router_free(router);
464 }
465 }
466
467 #[test]
468 fn a_table_sized_by_the_caller_holds_what_it_was_asked_for() {
469 // Safety: the handle is created and released here.
470 unsafe {
471 let router = pamoja_router_new(0x01, 3);
472 assert_eq!(pamoja_router_capacity(router), 3);
473 for node in 0..10u32 {
474 pamoja_router_observe(router, node + 0x100, 0x05, 4);
475 }
476 assert_eq!(pamoja_router_len(router), 3);
477 pamoja_router_free(router);
478 }
479 }
480
481 #[test]
482 fn the_table_fills_to_its_capacity() {
483 let router = pamoja_router_new(0x01, PAMOJA_ROUTING_DEFAULT_CAPACITY);
484 // Safety: the handle was just created.
485 unsafe {
486 for node in 0..PAMOJA_ROUTING_DEFAULT_CAPACITY + 8 {
487 pamoja_router_observe(router, node as u32 + 0x100, 0x05, 4);
488 }
489 assert_eq!(pamoja_router_len(router), PAMOJA_ROUTING_DEFAULT_CAPACITY);
490 assert_eq!(
491 pamoja_router_capacity(router),
492 PAMOJA_ROUTING_DEFAULT_CAPACITY
493 );
494 pamoja_router_free(router);
495 }
496 }
497
498 #[test]
499 fn every_route_held_is_read_by_its_position() {
500 let router = pamoja_router_new(0x01, 2);
501 // Safety: the handle was just created and the out-pointer is valid.
502 unsafe {
503 pamoja_router_observe(router, 0x0A, 0x05, 3);
504 pamoja_router_observe(router, 0x20, 0x03, 5);
505 assert!(pamoja_router_observe(router, 0x0B, 0x07, 2));
506
507 let mut held = Vec::new();
508 let mut route = PamojaRoute {
509 dst: 0,
510 next_hop: 0,
511 cost: 0,
512 };
513 for index in 0..pamoja_router_len(router) {
514 assert!(pamoja_router_route_at(router, index, &mut route));
515 held.push((route.dst, route.next_hop, route.cost));
516 }
517 assert_eq!(held, [(0x0A, 0x05, 3), (0x0B, 0x07, 2)]);
518 assert!(!pamoja_router_route_at(router, 2, &mut route));
519 assert!(!pamoja_router_route_at(router, 0, ptr::null_mut()));
520 pamoja_router_free(router);
521 }
522 }
523
524 #[test]
525 fn null_handles_are_tolerated() {
526 // Safety: every call below is documented to accept null.
527 unsafe {
528 assert_eq!(pamoja_router_address(ptr::null()), 0);
529 assert!(!pamoja_router_observe(ptr::null_mut(), 1, 2, 3));
530 assert_eq!(
531 pamoja_router_forward(ptr::null(), 1, ptr::null_mut()),
532 PamojaForward::Flood
533 );
534 pamoja_router_forget(ptr::null_mut(), 1);
535 assert_eq!(pamoja_router_len(ptr::null()), 0);
536 pamoja_router_free(ptr::null_mut());
537 }
538 }
539}