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 neighbour 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`] recognises 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 neighbour, that neighbour 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 neighbour 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 neighbour 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 neighbour 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/// Decides what to do with a packet bound for a node.
233///
234/// # Arguments
235///
236/// * `router` - the routing table.
237/// * `dst` - the node the packet is addressed to.
238/// * `out_next_hop` - receives the neighbour to unicast to, written only when the
239///   answer is [`PamojaForward::Relay`].
240///
241/// # Returns
242///
243/// [`PamojaForward::Deliver`] when the packet is for this node,
244/// [`PamojaForward::Relay`] when a route is known, or [`PamojaForward::Flood`]
245/// when none is, which hands the packet back to the flooding layer. A null router
246/// answers [`PamojaForward::Flood`], the choice that always works.
247///
248/// # Safety
249///
250/// `router` must be a live handle from [`pamoja_router_new`], or null, and
251/// `out_next_hop` must point to a writable `uint32_t` or be null.
252#[no_mangle]
253pub unsafe extern "C" fn pamoja_router_forward(
254    router: *const PamojaRouter,
255    dst: u32,
256    out_next_hop: *mut u32,
257) -> PamojaForward {
258    if !out_next_hop.is_null() {
259        *out_next_hop = 0;
260    }
261    if router.is_null() {
262        return PamojaForward::Flood;
263    }
264    match (*router).router.forward(dst) {
265        Forward::Deliver => PamojaForward::Deliver,
266        Forward::Relay(next_hop) => {
267            if !out_next_hop.is_null() {
268                *out_next_hop = next_hop;
269            }
270            PamojaForward::Relay
271        }
272        Forward::Flood => PamojaForward::Flood,
273    }
274}
275
276/// Forgets the route to a node, for example after it stops answering.
277///
278/// # Arguments
279///
280/// * `router` - the routing table.
281/// * `dst` - the node to forget.
282///
283/// # Safety
284///
285/// `router` must be a live handle from [`pamoja_router_new`], or null.
286#[no_mangle]
287pub unsafe extern "C" fn pamoja_router_forget(router: *mut PamojaRouter, dst: u32) {
288    if router.is_null() {
289        return;
290    }
291    (*router).router.forget(dst);
292}
293
294/// Returns how many routes a table currently holds.
295///
296/// # Returns
297///
298/// The number of routes, or 0 if `router` is null.
299///
300/// # Safety
301///
302/// `router` must be a live handle from [`pamoja_router_new`], or null.
303#[no_mangle]
304pub unsafe extern "C" fn pamoja_router_len(router: *const PamojaRouter) -> usize {
305    if router.is_null() {
306        return 0;
307    }
308    (*router).router.len()
309}
310
311/// Returns how many routes a table can hold.
312///
313/// # Returns
314///
315/// The capacity it was created with, or 0 if `router` is null.
316///
317/// # Safety
318///
319/// `router` must be a live handle from [`pamoja_router_new`], or null.
320#[no_mangle]
321pub unsafe extern "C" fn pamoja_router_capacity(router: *const PamojaRouter) -> usize {
322    if router.is_null() {
323        return 0;
324    }
325    (*router).router.capacity()
326}
327
328/// Releases a routing table handle.
329///
330/// Passing null is a no-op.
331///
332/// # Safety
333///
334/// `router` must be a handle from [`pamoja_router_new`] that has not already been
335/// freed, or null. After this call it must not be used again.
336#[no_mangle]
337pub unsafe extern "C" fn pamoja_router_free(router: *mut PamojaRouter) {
338    if !router.is_null() {
339        drop(Box::from_raw(router));
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use std::ptr;
346
347    use super::*;
348
349    #[test]
350    fn a_cheaper_neighbour_replaces_the_route() {
351        let router = pamoja_router_new(0x01, PAMOJA_ROUTING_DEFAULT_CAPACITY);
352        // Safety: the handle was just created and the out-pointers are valid.
353        unsafe {
354            assert_eq!(pamoja_router_address(router), 0x01);
355            assert!(pamoja_router_observe(router, 0x09, 0x05, 2));
356
357            let mut next_hop = 0u32;
358            assert_eq!(
359                pamoja_router_forward(router, 0x09, &mut next_hop),
360                PamojaForward::Relay
361            );
362            assert_eq!(next_hop, 0x05);
363
364            assert!(pamoja_router_observe(router, 0x09, 0x07, 1));
365            let mut route = PamojaRoute {
366                dst: 0,
367                next_hop: 0,
368                cost: 0,
369            };
370            assert!(pamoja_router_route(router, 0x09, &mut route));
371            assert_eq!(
372                route,
373                PamojaRoute {
374                    dst: 0x09,
375                    next_hop: 0x07,
376                    cost: 1
377                }
378            );
379            assert_eq!(pamoja_router_len(router), 1);
380            pamoja_router_free(router);
381        }
382    }
383
384    #[test]
385    fn a_packet_for_this_node_is_delivered_and_an_unknown_one_floods() {
386        let router = pamoja_router_new(0x01, PAMOJA_ROUTING_DEFAULT_CAPACITY);
387        // Safety: the handle was just created and the out-pointer is valid.
388        unsafe {
389            let mut next_hop = 0xFFFF_FFFFu32;
390            assert_eq!(
391                pamoja_router_forward(router, 0x01, &mut next_hop),
392                PamojaForward::Deliver
393            );
394            assert_eq!(next_hop, 0, "no next hop belongs to a local delivery");
395            assert_eq!(
396                pamoja_router_forward(router, 0x20, &mut next_hop),
397                PamojaForward::Flood
398            );
399            pamoja_router_free(router);
400        }
401    }
402
403    #[test]
404    fn a_forgotten_route_floods_again() {
405        let router = pamoja_router_new(0x01, PAMOJA_ROUTING_DEFAULT_CAPACITY);
406        // Safety: the handle was just created and the out-pointers are valid.
407        unsafe {
408            pamoja_router_observe(router, 0x09, 0x05, 2);
409            let mut cost = 0u16;
410            assert!(pamoja_router_cost(router, 0x09, &mut cost));
411            assert_eq!(cost, 2);
412
413            pamoja_router_forget(router, 0x09);
414            assert_eq!(pamoja_router_len(router), 0);
415            let mut next_hop = 0u32;
416            assert!(!pamoja_router_next_hop(router, 0x09, &mut next_hop));
417            assert_eq!(
418                pamoja_router_forward(router, 0x09, ptr::null_mut()),
419                PamojaForward::Flood
420            );
421            pamoja_router_free(router);
422        }
423    }
424
425    #[test]
426    fn a_table_sized_by_the_caller_holds_what_it_was_asked_for() {
427        // Safety: the handle is created and released here.
428        unsafe {
429            let router = pamoja_router_new(0x01, 3);
430            assert_eq!(pamoja_router_capacity(router), 3);
431            for node in 0..10u32 {
432                pamoja_router_observe(router, node + 0x100, 0x05, 4);
433            }
434            assert_eq!(pamoja_router_len(router), 3);
435            pamoja_router_free(router);
436        }
437    }
438
439    #[test]
440    fn the_table_fills_to_its_capacity() {
441        let router = pamoja_router_new(0x01, PAMOJA_ROUTING_DEFAULT_CAPACITY);
442        // Safety: the handle was just created.
443        unsafe {
444            for node in 0..PAMOJA_ROUTING_DEFAULT_CAPACITY + 8 {
445                pamoja_router_observe(router, node as u32 + 0x100, 0x05, 4);
446            }
447            assert_eq!(pamoja_router_len(router), PAMOJA_ROUTING_DEFAULT_CAPACITY);
448            assert_eq!(
449                pamoja_router_capacity(router),
450                PAMOJA_ROUTING_DEFAULT_CAPACITY
451            );
452            pamoja_router_free(router);
453        }
454    }
455
456    #[test]
457    fn null_handles_are_tolerated() {
458        // Safety: every call below is documented to accept null.
459        unsafe {
460            assert_eq!(pamoja_router_address(ptr::null()), 0);
461            assert!(!pamoja_router_observe(ptr::null_mut(), 1, 2, 3));
462            assert_eq!(
463                pamoja_router_forward(ptr::null(), 1, ptr::null_mut()),
464                PamojaForward::Flood
465            );
466            pamoja_router_forget(ptr::null_mut(), 1);
467            assert_eq!(pamoja_router_len(ptr::null()), 0);
468            pamoja_router_free(ptr::null_mut());
469        }
470    }
471}