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}