myrmic-sdk 0.7.0

SDK for writing Myrmic cells (WASM modules deployed and managed by the sorg infrastructure): host function bindings for db, tap, outlet, gpio, ble, gateway, cell messaging, logging and time, plus the #[cmd]/#[monitor] export macros.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
use serde::Serialize;
use serde::de::DeserializeOwned;

use crate::{Bytes, Result};
use alloc::string::String;
use myrmic_common::cells::Sri;

/// Turns a raw payload buffer into `Self`.
///
/// Message types get this for free via `#[derive(Message)]`, which delegates to
/// the bound [`Codec`]. The `#[cmd]`/`#[evt]` handler macros call
/// [`from_args`](Decoder::from_args) on the handler's argument type.
pub trait Decoder: Sized {
    /// Reads this invocation's `length`-byte payload from the host (via
    /// [`get_arguments`](crate::get_arguments)) and decodes it with
    /// [`from_bytes`](Self::from_bytes).
    ///
    /// A failure on an empty buffer is reported as a missing payload instead: a
    /// decoder for a mandatory payload can only describe absence as malformed
    /// input, which points at nothing the handler's author can act on.
    fn from_args(length: usize) -> Result<Self> {
        let mut bytes = alloc::vec![0u8; length];
        let n = crate::get_arguments(&mut bytes).map_err(|_| "failed to read arguments")?;
        bytes.truncate(n);
        let absent = bytes.is_empty();

        Self::from_bytes(bytes).map_err(|err| {
            if absent {
                "no payload was sent; declare the handler's payload as `Option<_>` \
                 to accept an invocation sent without one"
            } else {
                err
            }
        })
    }

    /// Decodes `Self` from the raw payload buffer.
    fn from_bytes(bytes: Bytes) -> Result<Self>;
}

/// Turns `Self` into a raw payload buffer.
///
/// Message types get this for free via `#[derive(Message)]`, which delegates to
/// the bound [`Codec`].
pub trait Encoder {
    /// Encodes `self` into a raw payload buffer.
    fn to_bytes(&self) -> Result<Bytes>;
}

/// A wire serialization format.
///
/// Implement this to add a custom codec, then bind it to a message type with
/// `#[derive(Message)]` + `#[codec(YourCodec)]`. The SDK ships [`Json`] and
/// [`Postcard`].
///
/// ```
/// struct MsgPack;
/// impl myrmic_sdk::Codec for MsgPack {
///     fn encode<T: serde::Serialize + ?Sized>(value: &T) -> myrmic_sdk::Result<myrmic_sdk::Bytes> {
///         rmp_serde::to_vec(value).map_err(|_| "encode failed")
///     }
///     fn decode<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> myrmic_sdk::Result<T> {
///         rmp_serde::from_slice(bytes).map_err(|_| "decode failed")
///     }
/// }
///
/// use myrmic_sdk::Codec;
/// let bytes = MsgPack::encode(&42u32)?;
/// assert_eq!(MsgPack::decode::<u32>(&bytes)?, 42);
/// # Ok::<(), &'static str>(())
/// ```
pub trait Codec {
    /// Serializes `value` into bytes.
    fn encode<T: Serialize + ?Sized>(value: &T) -> Result<Bytes>;
    /// Deserializes a `T` from `bytes`.
    fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T>;
}

/// JSON codec.
pub struct Json;

impl Codec for Json {
    fn encode<T: Serialize + ?Sized>(value: &T) -> Result<Bytes> {
        serde_json::to_vec(value).map_err(|_| "failed to serialize json")
    }

    fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
        serde_json::from_slice(bytes).map_err(|_| "failed to deserialize json")
    }
}

/// Postcard codec.
pub struct Postcard;

impl Codec for Postcard {
    fn encode<T: Serialize + ?Sized>(value: &T) -> Result<Bytes> {
        postcard::to_allocvec(value).map_err(|_| "failed to serialize postcard")
    }

    fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
        postcard::from_bytes(bytes).map_err(|_| "failed to deserialize postcard")
    }
}

/// Raw, uncodec'd bytes — a handler parameter of type `Bytes` receives the
/// payload verbatim.
impl Decoder for Bytes {
    fn from_bytes(bytes: Bytes) -> Result<Self> {
        Ok(bytes)
    }
}

/// Used to send custom payloads, for example, raw image data, etc.
impl Encoder for Bytes {
    fn to_bytes(&self) -> Result<Bytes> {
        Ok(self.clone())
    }
}

impl Decoder for Sri {
    fn from_bytes(bytes: Bytes) -> Result<Self> {
        let (hi, lo): (i64, i64) = <Postcard as Codec>::decode(&bytes)?;
        Ok(Sri::from_parts(hi, lo))
    }
}

impl Encoder for Sri {
    fn to_bytes(&self) -> Result<Bytes> {
        let parts = self.to_parts();
        <Postcard as Codec>::encode(&parts)
    }
}

/// The absence of a payload. This is the default [`Decoder`] the `#[cmd]` /
/// `#[evt]` macros use for a handler declared with only a `Metadata` parameter:
/// decoding succeeds only when the argument buffer is empty, so sending a
/// payload to such a handler is rejected rather than silently ignored.
pub struct Void;

impl Decoder for Void {
    fn from_args(length: usize) -> Result<Self> {
        if length == 0 {
            Ok(Void)
        } else {
            Err("this handler does not accept a payload")
        }
    }

    fn from_bytes(bytes: Bytes) -> Result<Self> {
        if bytes.is_empty() {
            Ok(Void)
        } else {
            Err("this handler does not accept a payload")
        }
    }
}

impl Encoder for Void {
    fn to_bytes(&self) -> Result<Bytes> {
        Ok(Bytes::new())
    }
}

impl<T: Decoder> Decoder for Option<T> {
    fn from_args(length: usize) -> Result<Self> {
        if length == 0 {
            Ok(None)
        } else {
            T::from_args(length).map(Some)
        }
    }

    fn from_bytes(bytes: Bytes) -> Result<Self> {
        if bytes.is_empty() {
            Ok(None)
        } else {
            T::from_bytes(bytes).map(Some)
        }
    }
}

impl<T: Encoder> Encoder for Option<T> {
    fn to_bytes(&self) -> Result<Bytes> {
        match self {
            Some(value) => T::to_bytes(value),
            None => Ok(Bytes::new()),
        }
    }
}

/// Bare `String`, `char`, `bool`, and float payloads, carried on the wire as
/// JSON.
///
/// A handler parameter — or a `send`/`publish`/callback value — of one of these
/// types is encoded directly, with no wrapper message struct, so a bare scalar
/// travels exactly as an external caller (e.g. the gateway) would naturally send
/// it: `true`, `"hi"`, `1.5`. This matches the [`Json`] default that
/// `#[derive(Message)]` gives struct payloads.
macro_rules! json_scalar {
    ($($ty:ty),* $(,)?) => {$(
        impl Decoder for $ty {
            fn from_bytes(bytes: Bytes) -> Result<Self> {
                <Json as Codec>::decode(&bytes)
            }
        }

        impl Encoder for $ty {
            fn to_bytes(&self) -> Result<Bytes> {
                <Json as Codec>::encode(self)
            }
        }
    )*};
}

// `f32`/`f64` fall here too: `serde_json` already coerces an integer literal
// (`42`) into a float, so a plain decode covers both `42` and `1.5`.
json_scalar!(String, bool, char, f32, f64);

/// Bare integer payloads, carried on the wire as JSON numbers.
///
/// An exact decode is tried first so a full-width integer literal (including
/// 128-bit) round-trips losslessly. Failing that, the payload is read as a
/// [`serde_json::Number`] and accepted when its value is a whole number in range
/// for the target type — so a `42.0` sent for a `u32` still decodes to `42`,
/// while `42.5` or an out-of-range value is rejected.
macro_rules! json_int {
    ($($ty:ty),* $(,)?) => {$(
        impl Decoder for $ty {
            fn from_bytes(bytes: Bytes) -> Result<Self> {
                if let Ok(value) = <Json as Codec>::decode::<$ty>(&bytes) {
                    return Ok(value);
                }
                let number: serde_json::Number = <Json as Codec>::decode(&bytes)?;
                let f = number.as_f64().ok_or("expected a number")?;
                // `f as i128` round-tripping equal proves `f` is a whole number
                // within i128 range; the bounds check then confines it to `$ty`.
                // (`f64::fract` is unavailable under `no_std`.)
                if f as i128 as f64 == f && f >= <$ty>::MIN as f64 && f <= <$ty>::MAX as f64 {
                    Ok(f as $ty)
                } else {
                    Err("number is not a whole value in range for the target type")
                }
            }
        }

        impl Encoder for $ty {
            fn to_bytes(&self) -> Result<Bytes> {
                <Json as Codec>::encode(self)
            }
        }
    )*};
}

json_int!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128,);

#[cfg(test)]
mod tests {
    use core::ffi::c_int;

    use spin::Mutex;

    use super::{Decoder, Encoder};
    use crate::{Bytes, Callback, JsonValue, Result};
    use alloc::string::String;
    use alloc::vec::Vec;
    use myrmic_common::cells::Command;

    fn dec<T: Decoder>(bytes: &[u8]) -> Result<T> {
        T::from_bytes(Bytes::from(bytes))
    }

    fn enc<T: Encoder>(value: &T) -> Vec<u8> {
        value.to_bytes().unwrap()
    }

    fn enc_str<T: Encoder>(value: &T) -> String {
        String::from_utf8(enc(value)).unwrap()
    }

    #[test]
    fn integer_encodes_as_json_number() {
        assert_eq!(enc_str(&42u32), "42");
    }

    #[test]
    fn json_number_decodes_into_integer() {
        // A bare `42` as the gateway sends it on the wire.
        assert_eq!(dec::<u32>(b"42").unwrap(), 42);
    }

    #[test]
    fn integral_float_decodes_into_integer() {
        assert_eq!(dec::<u32>(b"42.0").unwrap(), 42);
    }

    #[test]
    fn fractional_number_rejected_for_integer() {
        assert!(dec::<u32>(b"42.5").is_err());
    }

    #[test]
    fn out_of_range_number_rejected_for_integer() {
        assert!(dec::<u8>(b"300").is_err());
    }

    #[test]
    fn signed_integer_round_trips() {
        assert_eq!(dec::<i64>(&enc(&-5i64)).unwrap(), -5);
    }

    #[test]
    fn max_u128_round_trips_exactly() {
        let v = u128::MAX;
        assert_eq!(dec::<u128>(&enc(&v)).unwrap(), v);
    }

    #[test]
    fn float_decodes_from_any_json_number() {
        assert_eq!(dec::<f64>(b"42").unwrap(), 42.0);
        assert_eq!(dec::<f32>(b"1.5").unwrap(), 1.5);
    }

    #[test]
    fn float_encodes_as_json_number() {
        assert_eq!(enc_str(&1.5f64), "1.5");
    }

    #[test]
    fn bool_round_trips_as_json() {
        assert_eq!(enc_str(&true), "true");
        assert!(dec::<bool>(b"true").unwrap());
    }

    #[test]
    fn char_round_trips_as_json_string() {
        assert_eq!(enc_str(&'a'), "\"a\"");
        assert_eq!(dec::<char>(b"\"a\"").unwrap(), 'a');
    }

    #[test]
    fn string_round_trips_as_json_string() {
        // A bareword the gateway wraps as a JSON string.
        assert_eq!(enc_str(&String::from("jsontest")), "\"jsontest\"");
        assert_eq!(dec::<String>(b"\"jsontest\"").unwrap(), "jsontest");
    }

    #[test]
    fn optional_callback_absorbs_only_the_empty_buffer() {
        // What `myrmic send <cell> count` puts on the wire.
        assert!(dec::<Option<Callback<JsonValue>>>(b"").unwrap().is_none());

        let decoded = dec::<Option<Callback<JsonValue>>>(b"on_reply").unwrap();
        assert_eq!(Command::from(decoded.unwrap()).as_ref(), "on_reply");

        // A non-empty buffer that is not a command name still fails: the
        // optional form absorbs absence, never malformedness. `on_reply`
        // without `--raw` arrives JSON-quoted, which is what this is.
        assert!(dec::<Option<Callback<JsonValue>>>(b"\"on_reply\"").is_err());

        // The bare form still rejects the empty buffer.
        assert!(dec::<Callback<JsonValue>>(b"").is_err());
    }

    #[test]
    fn zero_length_from_args_never_touches_the_inner_decoder() {
        // `Probe` panics from both of its methods, so a branch that delegated
        // to `T` would fail loudly instead of returning `None`. These come
        // first so that failure is the one a broken branch reports.
        assert!(<Option<Probe>>::from_args(0).unwrap().is_none());
        assert!(<Option<Probe>>::from_bytes(Bytes::new()).unwrap().is_none());
    }

    #[test]
    fn an_absent_payload_is_reported_with_advice() {
        // An invocation carrying nothing, which is what `myrmic send <cell>
        // <cmd>` puts on the wire. No mandatory payload can decode that,
        // whatever its type, so the failure names the fix instead.
        assert_eq!(
            from_args_with::<u32>(b"").unwrap_err(),
            "no payload was sent; declare the handler's payload as `Option<_>` \
             to accept an invocation sent without one"
        );
        assert_eq!(
            from_args_with::<Callback<JsonValue>>(b"").unwrap_err(),
            "no payload was sent; declare the handler's payload as `Option<_>` \
             to accept an invocation sent without one"
        );

        // The optional form absorbs absence, and stays silent about it.
        assert!(from_args_with::<Option<u32>>(b"").unwrap().is_none());
        assert_eq!(from_args_with::<Option<u32>>(b"42").unwrap(), Some(42));

        // A payload that did arrive keeps its decoder's own error: the advice
        // is about absence, not about malformedness.
        assert_eq!(
            from_args_with::<u32>(b"42.5").unwrap_err(),
            "number is not a whole value in range for the target type"
        );
        assert_eq!(
            from_args_with::<Callback<JsonValue>>(b"\"on_reply\"").unwrap_err(),
            "name can only contain ASCII alphanumeric characters and underscores"
        );
    }

    /// A decoder that fails loudly if it is ever reached.
    struct Probe;

    impl Decoder for Probe {
        fn from_args(_length: usize) -> Result<Self> {
            panic!("a zero-length argument buffer was delegated to the inner decoder");
        }

        fn from_bytes(_bytes: Bytes) -> Result<Self> {
            panic!("an empty byte buffer was delegated to the inner decoder");
        }
    }

    /// Decodes `payload` through the real [`Decoder::from_args`] path, as a host
    /// invocation carrying it would.
    fn from_args_with<T: Decoder>(payload: &[u8]) -> Result<T> {
        *ARGUMENTS.lock() = payload.to_vec();

        T::from_args(payload.len())
    }

    /// What the stubbed `get_arguments` below hands back.
    static ARGUMENTS: Mutex<Vec<u8>> = Mutex::new(Vec::new());

    /// Stands in for the host's `arguments` import, which a test binary has no
    /// other definition of.
    #[unsafe(no_mangle)]
    extern "C" fn get_arguments(buffer: *mut u8, length: c_int) -> c_int {
        let payload = ARGUMENTS.lock();
        let n = payload.len().min(length as usize);
        // SAFETY: the caller guarantees `buffer` is writable for `length`
        // bytes, and `n` is capped at `length`.
        unsafe { core::ptr::copy_nonoverlapping(payload.as_ptr(), buffer, n) };

        n as c_int
    }
}