Skip to main content

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}