Skip to main content

pamoja_ffi/
mavlink_schema.rs

1//! The C ABI for MAVLink message shapes: reading and writing a message by field name.
2//!
3//! [`mavlink`](crate::mavlink) carries any message's bytes, which is enough to move traffic
4//! but leaves the caller hand-packing payloads against a message definition. This is the
5//! layer above: a schema states a message's fields, and a message reads and writes them by
6//! name, so a caller works in `custom_mode` and `lat` rather than byte offsets.
7//!
8//! Two sources of shape are supported and behave identically. Every message the engine
9//! types is published, so [`pamoja_mavlink_schema_for_id`] resolves the common dialect with
10//! nothing to declare. A message from ArduPilot's dialect, PX4's, or a vendor's private one
11//! is described through [`PamojaMavlinkSchemaBuilder`], which puts the fields in wire order
12//! and derives the `CRC_EXTRA` seed, so a caller transcribes a definition as it reads.
13
14use std::ffi::{c_char, CString};
15
16use pamoja_mavlink::dialect::{
17    descriptor, descriptor_by_name, DynamicMessage, FieldType, MessageDescriptor,
18    MessageDescriptorBuilder, OwnedMessageDescriptor, DESCRIPTORS,
19};
20use pamoja_mavlink::{Header, Result as MavlinkResult};
21
22use crate::mavlink::{status_of, PamojaMavlinkDialect, PamojaMavlinkFrame, PamojaMavlinkHeader};
23use crate::{read_bytes, read_str, set_last_error, PamojaStatus};
24
25/// A `uint8_t` field.
26pub const PAMOJA_MAVLINK_FIELD_UINT8: u32 = 1;
27/// An `int8_t` field.
28pub const PAMOJA_MAVLINK_FIELD_INT8: u32 = 2;
29/// A `char` field; an array of these carries text.
30pub const PAMOJA_MAVLINK_FIELD_CHAR: u32 = 3;
31/// A `uint16_t` field.
32pub const PAMOJA_MAVLINK_FIELD_UINT16: u32 = 4;
33/// An `int16_t` field.
34pub const PAMOJA_MAVLINK_FIELD_INT16: u32 = 5;
35/// A `uint32_t` field.
36pub const PAMOJA_MAVLINK_FIELD_UINT32: u32 = 6;
37/// An `int32_t` field.
38pub const PAMOJA_MAVLINK_FIELD_INT32: u32 = 7;
39/// A `uint64_t` field.
40pub const PAMOJA_MAVLINK_FIELD_UINT64: u32 = 8;
41/// An `int64_t` field.
42pub const PAMOJA_MAVLINK_FIELD_INT64: u32 = 9;
43/// A `float` field.
44pub const PAMOJA_MAVLINK_FIELD_FLOAT: u32 = 10;
45/// A `double` field.
46pub const PAMOJA_MAVLINK_FIELD_DOUBLE: u32 = 11;
47
48/// Maps a field type onto its stable code.
49///
50/// The codes are written out rather than taken from a Rust enum's discriminants, so a
51/// value that crosses the boundary means the same thing in every build.
52fn code_of(ty: FieldType) -> u32 {
53    match ty {
54        FieldType::U8 => PAMOJA_MAVLINK_FIELD_UINT8,
55        FieldType::I8 => PAMOJA_MAVLINK_FIELD_INT8,
56        FieldType::Char => PAMOJA_MAVLINK_FIELD_CHAR,
57        FieldType::U16 => PAMOJA_MAVLINK_FIELD_UINT16,
58        FieldType::I16 => PAMOJA_MAVLINK_FIELD_INT16,
59        FieldType::U32 => PAMOJA_MAVLINK_FIELD_UINT32,
60        FieldType::I32 => PAMOJA_MAVLINK_FIELD_INT32,
61        FieldType::U64 => PAMOJA_MAVLINK_FIELD_UINT64,
62        FieldType::I64 => PAMOJA_MAVLINK_FIELD_INT64,
63        FieldType::F32 => PAMOJA_MAVLINK_FIELD_FLOAT,
64        FieldType::F64 => PAMOJA_MAVLINK_FIELD_DOUBLE,
65    }
66}
67
68/// Resolves a field type code, reporting an unknown one as an error.
69fn type_of(code: u32) -> Result<FieldType, PamojaStatus> {
70    Ok(match code {
71        PAMOJA_MAVLINK_FIELD_UINT8 => FieldType::U8,
72        PAMOJA_MAVLINK_FIELD_INT8 => FieldType::I8,
73        PAMOJA_MAVLINK_FIELD_CHAR => FieldType::Char,
74        PAMOJA_MAVLINK_FIELD_UINT16 => FieldType::U16,
75        PAMOJA_MAVLINK_FIELD_INT16 => FieldType::I16,
76        PAMOJA_MAVLINK_FIELD_UINT32 => FieldType::U32,
77        PAMOJA_MAVLINK_FIELD_INT32 => FieldType::I32,
78        PAMOJA_MAVLINK_FIELD_UINT64 => FieldType::U64,
79        PAMOJA_MAVLINK_FIELD_INT64 => FieldType::I64,
80        PAMOJA_MAVLINK_FIELD_FLOAT => FieldType::F32,
81        PAMOJA_MAVLINK_FIELD_DOUBLE => FieldType::F64,
82        _ => {
83            set_last_error(format!("{code} is not a MAVLink field type"));
84            return Err(PamojaStatus::InvalidArgument);
85        }
86    })
87}
88
89/// One field of a message shape, as read back from a schema.
90///
91/// Both names point into the schema they came from and stay valid until it is released, so
92/// a caller reads them in place rather than freeing them.
93#[repr(C)]
94#[derive(Clone, Copy, Debug)]
95pub struct PamojaMavlinkFieldInfo {
96    /// The field name as the dialect writes it, such as `custom_mode`.
97    pub name: *const c_char,
98    /// The field's type name as the dialect writes it, such as `uint32_t`.
99    pub type_name: *const c_char,
100    /// The field's type, one of the `PAMOJA_MAVLINK_FIELD_*` codes.
101    pub field_type: u32,
102    /// The element count for an array field, or `0` for a scalar.
103    pub array_len: u8,
104    /// `1` for a MAVLink 2 extension field, `0` for a base field.
105    pub extension: u8,
106    /// The field's byte offset within the payload.
107    pub offset: usize,
108}
109
110/// The shape of one message: its id, name, seed, and fields.
111///
112/// A schema is what turns bytes into named fields. It comes either from the built-in
113/// registry of typed messages or from a [`PamojaMavlinkSchemaBuilder`], and behaves the
114/// same way whichever it is.
115pub struct PamojaMavlinkSchema {
116    shape: OwnedMessageDescriptor,
117    name: CString,
118    field_names: Vec<CString>,
119}
120
121impl PamojaMavlinkSchema {
122    /// Moves a shape onto the heap, interning its names for the C side to borrow.
123    fn into_handle(shape: OwnedMessageDescriptor) -> Result<*mut Self, PamojaStatus> {
124        let interior = || {
125            set_last_error("a message or field name contains an interior null byte".to_owned());
126            PamojaStatus::InvalidArgument
127        };
128        let name = CString::new(shape.name()).map_err(|_| interior())?;
129        let field_names = shape
130            .fields()
131            .iter()
132            .map(|field| CString::new(field.name.as_str()))
133            .collect::<std::result::Result<Vec<_>, _>>()
134            .map_err(|_| interior())?;
135        Ok(Box::into_raw(Box::new(Self {
136            shape,
137            name,
138            field_names,
139        })))
140    }
141}
142
143/// Returns the shape of a message the engine types, by id.
144///
145/// # Arguments
146///
147/// * `msgid` - the message id to look up.
148/// * `out_schema` - set to a new schema handle on success, which the caller releases with
149///   [`pamoja_mavlink_schema_free`].
150///
151/// # Returns
152///
153/// [`PamojaStatus::Ok`] on success.
154///
155/// # Errors
156///
157/// Returns [`PamojaStatus::InvalidArgument`] if `out_schema` is null, and
158/// [`PamojaStatus::Unsupported`] for an id this build does not type, which is what
159/// [`PamojaMavlinkSchemaBuilder`] is for.
160///
161/// # Safety
162///
163/// `out_schema` must point at writable storage for one pointer.
164#[no_mangle]
165pub unsafe extern "C" fn pamoja_mavlink_schema_for_id(
166    msgid: u32,
167    out_schema: *mut *mut PamojaMavlinkSchema,
168) -> PamojaStatus {
169    if out_schema.is_null() {
170        set_last_error("out_schema must not be null".to_owned());
171        return PamojaStatus::InvalidArgument;
172    }
173    let Some(shape) = descriptor(msgid) else {
174        set_last_error(format!("message {msgid} is not one this build types"));
175        return PamojaStatus::Unsupported;
176    };
177    publish(shape, out_schema)
178}
179
180/// Returns the shape of a message the engine types, by name.
181///
182/// # Arguments
183///
184/// * `name` - the message name, such as `GLOBAL_POSITION_INT`.
185/// * `out_schema` - set to a new schema handle on success, which the caller releases with
186///   [`pamoja_mavlink_schema_free`].
187///
188/// # Returns
189///
190/// [`PamojaStatus::Ok`] on success.
191///
192/// # Errors
193///
194/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null or `name` is not
195/// UTF-8, and [`PamojaStatus::Unsupported`] for a name this build does not type.
196///
197/// # Safety
198///
199/// `name` must be a null-terminated C string, and `out_schema` must point at writable
200/// storage for one pointer.
201#[no_mangle]
202pub unsafe extern "C" fn pamoja_mavlink_schema_for_name(
203    name: *const c_char,
204    out_schema: *mut *mut PamojaMavlinkSchema,
205) -> PamojaStatus {
206    if out_schema.is_null() {
207        set_last_error("out_schema must not be null".to_owned());
208        return PamojaStatus::InvalidArgument;
209    }
210    let Some(name) = read_str(name, "name") else {
211        return PamojaStatus::InvalidArgument;
212    };
213    let Some(shape) = descriptor_by_name(name) else {
214        set_last_error(format!("{name} is not a message this build types"));
215        return PamojaStatus::Unsupported;
216    };
217    publish(shape, out_schema)
218}
219
220/// Returns how many messages this build types.
221///
222/// Together with [`pamoja_mavlink_schema_at`] this enumerates the built-in registry, so a
223/// caller can discover what is available rather than guessing ids.
224///
225/// # Returns
226///
227/// The count.
228#[no_mangle]
229pub extern "C" fn pamoja_mavlink_schema_count() -> usize {
230    DESCRIPTORS.len()
231}
232
233/// Returns the shape of the message at a position in the built-in registry.
234///
235/// # Arguments
236///
237/// * `index` - the position, below [`pamoja_mavlink_schema_count`].
238/// * `out_schema` - set to a new schema handle on success, which the caller releases with
239///   [`pamoja_mavlink_schema_free`].
240///
241/// # Returns
242///
243/// [`PamojaStatus::Ok`] on success.
244///
245/// # Errors
246///
247/// Returns [`PamojaStatus::InvalidArgument`] if `out_schema` is null or `index` is past
248/// the end of the registry.
249///
250/// # Safety
251///
252/// `out_schema` must point at writable storage for one pointer.
253#[no_mangle]
254pub unsafe extern "C" fn pamoja_mavlink_schema_at(
255    index: usize,
256    out_schema: *mut *mut PamojaMavlinkSchema,
257) -> PamojaStatus {
258    if out_schema.is_null() {
259        set_last_error("out_schema must not be null".to_owned());
260        return PamojaStatus::InvalidArgument;
261    }
262    let Some(shape) = DESCRIPTORS.get(index) else {
263        set_last_error(format!("{index} is past the end of the registry"));
264        return PamojaStatus::InvalidArgument;
265    };
266    publish(shape, out_schema)
267}
268
269unsafe fn publish(
270    shape: &MessageDescriptor<'static>,
271    out_schema: *mut *mut PamojaMavlinkSchema,
272) -> PamojaStatus {
273    match PamojaMavlinkSchema::into_handle(OwnedMessageDescriptor::from_descriptor(shape)) {
274        Ok(handle) => {
275            *out_schema = handle;
276            PamojaStatus::Ok
277        }
278        Err(status) => status,
279    }
280}
281
282/// Returns the id of the message a schema describes.
283///
284/// # Arguments
285///
286/// * `schema` - the shape to read.
287///
288/// # Returns
289///
290/// The message id, or `0` if `schema` is null.
291///
292/// # Safety
293///
294/// `schema` must be a live schema handle or null.
295#[no_mangle]
296pub unsafe extern "C" fn pamoja_mavlink_schema_id(schema: *const PamojaMavlinkSchema) -> u32 {
297    schema.as_ref().map_or(0, |schema| schema.shape.id())
298}
299
300/// Returns the name of the message a schema describes.
301///
302/// The pointer borrows the schema and stays valid until it is released.
303///
304/// # Arguments
305///
306/// * `schema` - the shape to read.
307///
308/// # Returns
309///
310/// The name, or null if `schema` is null.
311///
312/// # Safety
313///
314/// `schema` must be a live schema handle or null.
315#[no_mangle]
316pub unsafe extern "C" fn pamoja_mavlink_schema_name(
317    schema: *const PamojaMavlinkSchema,
318) -> *const c_char {
319    schema
320        .as_ref()
321        .map_or(std::ptr::null(), |schema| schema.name.as_ptr())
322}
323
324/// Returns the `CRC_EXTRA` seed a schema implies.
325///
326/// # Arguments
327///
328/// * `schema` - the shape to read.
329///
330/// # Returns
331///
332/// The seed, or `0` if `schema` is null.
333///
334/// # Safety
335///
336/// `schema` must be a live schema handle or null.
337#[no_mangle]
338pub unsafe extern "C" fn pamoja_mavlink_schema_crc_extra(schema: *const PamojaMavlinkSchema) -> u8 {
339    schema.as_ref().map_or(0, |schema| schema.shape.crc_extra())
340}
341
342/// Returns the length of the message on the wire, extensions included.
343///
344/// # Arguments
345///
346/// * `schema` - the shape to read.
347///
348/// # Returns
349///
350/// The length in bytes, or `0` if `schema` is null.
351///
352/// # Safety
353///
354/// `schema` must be a live schema handle or null.
355#[no_mangle]
356pub unsafe extern "C" fn pamoja_mavlink_schema_wire_len(
357    schema: *const PamojaMavlinkSchema,
358) -> usize {
359    schema.as_ref().map_or(0, |schema| {
360        schema.shape.with_descriptor(|shape| shape.wire_len())
361    })
362}
363
364/// Returns how many fields a message has.
365///
366/// # Arguments
367///
368/// * `schema` - the shape to read.
369///
370/// # Returns
371///
372/// The field count, or `0` if `schema` is null.
373///
374/// # Safety
375///
376/// `schema` must be a live schema handle or null.
377#[no_mangle]
378pub unsafe extern "C" fn pamoja_mavlink_schema_field_count(
379    schema: *const PamojaMavlinkSchema,
380) -> usize {
381    schema
382        .as_ref()
383        .map_or(0, |schema| schema.shape.fields().len())
384}
385
386/// Describes one field of a message.
387///
388/// # Arguments
389///
390/// * `schema` - the shape to read.
391/// * `index` - the field position, in wire order, below
392///   [`pamoja_mavlink_schema_field_count`].
393/// * `out_field` - set to the field's description on success.
394///
395/// # Returns
396///
397/// [`PamojaStatus::Ok`] on success.
398///
399/// # Errors
400///
401/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null or `index` is past
402/// the end of the field list.
403///
404/// # Safety
405///
406/// `schema` must be a live schema handle, and `out_field` must point at writable storage
407/// for one [`PamojaMavlinkFieldInfo`].
408#[no_mangle]
409pub unsafe extern "C" fn pamoja_mavlink_schema_field(
410    schema: *const PamojaMavlinkSchema,
411    index: usize,
412    out_field: *mut PamojaMavlinkFieldInfo,
413) -> PamojaStatus {
414    let Some(schema) = schema.as_ref() else {
415        set_last_error("schema must not be null".to_owned());
416        return PamojaStatus::InvalidArgument;
417    };
418    if out_field.is_null() {
419        set_last_error("out_field must not be null".to_owned());
420        return PamojaStatus::InvalidArgument;
421    }
422    let Some(field) = schema.shape.fields().get(index) else {
423        set_last_error(format!("{index} is past the end of the field list"));
424        return PamojaStatus::InvalidArgument;
425    };
426    let offset = schema
427        .shape
428        .with_descriptor(|shape| shape.offset_of(&field.name))
429        .expect("a field of this shape has an offset in it");
430
431    *out_field = PamojaMavlinkFieldInfo {
432        name: schema.field_names[index].as_ptr(),
433        type_name: TYPE_NAMES[type_index(field.ty)].as_ptr().cast(),
434        field_type: code_of(field.ty),
435        array_len: field.array_len,
436        extension: u8::from(field.extension),
437        offset,
438    };
439    PamojaStatus::Ok
440}
441
442// The type names handed back as borrowed C strings. They are null-terminated here so a
443// caller can read them in place, the way it reads a field name out of a schema.
444const TYPE_NAMES: [&[u8]; 11] = [
445    b"uint8_t\0",
446    b"int8_t\0",
447    b"char\0",
448    b"uint16_t\0",
449    b"int16_t\0",
450    b"uint32_t\0",
451    b"int32_t\0",
452    b"uint64_t\0",
453    b"int64_t\0",
454    b"float\0",
455    b"double\0",
456];
457
458fn type_index(ty: FieldType) -> usize {
459    code_of(ty) as usize - 1
460}
461
462/// Releases a schema.
463///
464/// # Arguments
465///
466/// * `schema` - the handle to release; null is ignored.
467///
468/// # Safety
469///
470/// `schema` must have come from one of the schema constructors and must not be used
471/// afterwards.
472#[no_mangle]
473pub unsafe extern "C" fn pamoja_mavlink_schema_free(schema: *mut PamojaMavlinkSchema) {
474    if !schema.is_null() {
475        drop(Box::from_raw(schema));
476    }
477}
478
479/// Adds a schema's message to a dialect table, so frames carrying it check.
480///
481/// # Arguments
482///
483/// * `dialect` - the table to extend.
484/// * `schema` - the shape whose id and seed to add.
485///
486/// # Returns
487///
488/// [`PamojaStatus::Ok`] on success.
489///
490/// # Errors
491///
492/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null.
493///
494/// # Safety
495///
496/// `dialect` must be a live dialect handle and `schema` a live schema handle.
497#[no_mangle]
498pub unsafe extern "C" fn pamoja_mavlink_dialect_add_schema(
499    dialect: *mut PamojaMavlinkDialect,
500    schema: *const PamojaMavlinkSchema,
501) -> PamojaStatus {
502    let Some(schema) = schema.as_ref() else {
503        set_last_error("schema must not be null".to_owned());
504        return PamojaStatus::InvalidArgument;
505    };
506    crate::mavlink::pamoja_mavlink_dialect_add(dialect, schema.shape.id(), schema.shape.crc_extra())
507}
508
509/// Describes a message this build does not type, one field at a time.
510///
511/// Fields are added in the order the message definition lists them;
512/// [`pamoja_mavlink_schema_builder_build`] puts them in wire order and derives the
513/// `CRC_EXTRA` seed from the result.
514pub struct PamojaMavlinkSchemaBuilder {
515    builder: Option<MessageDescriptorBuilder>,
516}
517
518/// Starts describing a message.
519///
520/// # Arguments
521///
522/// * `msgid` - the message id on the wire.
523/// * `name` - the message name, which the seed derivation folds in, so it must match the
524///   dialect exactly.
525///
526/// # Returns
527///
528/// A builder the caller releases with [`pamoja_mavlink_schema_builder_free`] or consumes
529/// with [`pamoja_mavlink_schema_builder_build`], or null if `name` is null or not UTF-8.
530///
531/// # Safety
532///
533/// `name` must be a null-terminated C string.
534#[no_mangle]
535pub unsafe extern "C" fn pamoja_mavlink_schema_builder_new(
536    msgid: u32,
537    name: *const c_char,
538) -> *mut PamojaMavlinkSchemaBuilder {
539    let Some(name) = read_str(name, "name") else {
540        return std::ptr::null_mut();
541    };
542    Box::into_raw(Box::new(PamojaMavlinkSchemaBuilder {
543        builder: Some(MessageDescriptorBuilder::new(msgid, name)),
544    }))
545}
546
547/// Adds a base field, in the order the definition declares it.
548///
549/// # Arguments
550///
551/// * `builder` - the description to extend.
552/// * `name` - the field name.
553/// * `field_type` - the field's type, one of the `PAMOJA_MAVLINK_FIELD_*` codes.
554/// * `array_len` - the element count for an array, or `0` for a scalar.
555///
556/// # Returns
557///
558/// [`PamojaStatus::Ok`] on success.
559///
560/// # Errors
561///
562/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, `name` is not UTF-8, or
563/// `field_type` is not a MAVLink field type.
564///
565/// # Safety
566///
567/// `builder` must be a live builder handle and `name` a null-terminated C string.
568#[no_mangle]
569pub unsafe extern "C" fn pamoja_mavlink_schema_builder_field(
570    builder: *mut PamojaMavlinkSchemaBuilder,
571    name: *const c_char,
572    field_type: u32,
573    array_len: u8,
574) -> PamojaStatus {
575    add_field(builder, name, field_type, array_len, false)
576}
577
578/// Adds a MAVLink 2 extension field, in the order the definition declares it.
579///
580/// Extensions keep their declared order, stay out of the `CRC_EXTRA` seed, and read as zero
581/// from a frame sent by a peer that predates them.
582///
583/// # Arguments
584///
585/// * `builder` - the description to extend.
586/// * `name` - the field name.
587/// * `field_type` - the field's type, one of the `PAMOJA_MAVLINK_FIELD_*` codes.
588/// * `array_len` - the element count for an array, or `0` for a scalar.
589///
590/// # Returns
591///
592/// [`PamojaStatus::Ok`] on success.
593///
594/// # Errors
595///
596/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, `name` is not UTF-8, or
597/// `field_type` is not a MAVLink field type.
598///
599/// # Safety
600///
601/// `builder` must be a live builder handle and `name` a null-terminated C string.
602#[no_mangle]
603pub unsafe extern "C" fn pamoja_mavlink_schema_builder_extension(
604    builder: *mut PamojaMavlinkSchemaBuilder,
605    name: *const c_char,
606    field_type: u32,
607    array_len: u8,
608) -> PamojaStatus {
609    add_field(builder, name, field_type, array_len, true)
610}
611
612unsafe fn add_field(
613    builder: *mut PamojaMavlinkSchemaBuilder,
614    name: *const c_char,
615    field_type: u32,
616    array_len: u8,
617    extension: bool,
618) -> PamojaStatus {
619    let Some(handle) = builder.as_mut() else {
620        set_last_error("builder must not be null".to_owned());
621        return PamojaStatus::InvalidArgument;
622    };
623    let Some(name) = read_str(name, "name") else {
624        return PamojaStatus::InvalidArgument;
625    };
626    let ty = match type_of(field_type) {
627        Ok(ty) => ty,
628        Err(status) => return status,
629    };
630    let Some(builder) = handle.builder.take() else {
631        set_last_error("this builder has already been built".to_owned());
632        return PamojaStatus::InvalidArgument;
633    };
634    handle.builder = Some(if extension {
635        builder.extension(name, ty, array_len)
636    } else {
637        builder.field(name, ty, array_len)
638    });
639    PamojaStatus::Ok
640}
641
642/// Puts the declared fields in wire order and finishes the shape.
643///
644/// The builder is consumed and released whether or not the shape is valid, so it must not
645/// be used again.
646///
647/// # Arguments
648///
649/// * `builder` - the description to finish.
650/// * `out_schema` - set to a new schema handle on success, which the caller releases with
651///   [`pamoja_mavlink_schema_free`].
652///
653/// # Returns
654///
655/// [`PamojaStatus::Ok`] on success.
656///
657/// # Errors
658///
659/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, if two fields share a
660/// name, or if the fields do not fit a MAVLink payload.
661///
662/// # Safety
663///
664/// `builder` must be a live builder handle, and `out_schema` must point at writable
665/// storage for one pointer.
666#[no_mangle]
667pub unsafe extern "C" fn pamoja_mavlink_schema_builder_build(
668    builder: *mut PamojaMavlinkSchemaBuilder,
669    out_schema: *mut *mut PamojaMavlinkSchema,
670) -> PamojaStatus {
671    if builder.is_null() {
672        set_last_error("builder must not be null".to_owned());
673        return PamojaStatus::InvalidArgument;
674    }
675    if out_schema.is_null() {
676        set_last_error("out_schema must not be null".to_owned());
677        return PamojaStatus::InvalidArgument;
678    }
679    let handle = Box::from_raw(builder);
680    let Some(builder) = handle.builder else {
681        set_last_error("this builder has already been built".to_owned());
682        return PamojaStatus::InvalidArgument;
683    };
684    let shape = match builder.build() {
685        Ok(shape) => shape,
686        Err(error) => return status_of(error),
687    };
688    match PamojaMavlinkSchema::into_handle(shape) {
689        Ok(schema) => {
690            *out_schema = schema;
691            PamojaStatus::Ok
692        }
693        Err(status) => status,
694    }
695}
696
697/// Releases a builder that was never built.
698///
699/// # Arguments
700///
701/// * `builder` - the handle to release; null is ignored.
702///
703/// # Safety
704///
705/// `builder` must have come from [`pamoja_mavlink_schema_builder_new`], must not already
706/// have been built, and must not be used afterwards.
707#[no_mangle]
708pub unsafe extern "C" fn pamoja_mavlink_schema_builder_free(
709    builder: *mut PamojaMavlinkSchemaBuilder,
710) {
711    if !builder.is_null() {
712        drop(Box::from_raw(builder));
713    }
714}
715
716/// A message being written or read field by field against a schema.
717pub struct PamojaMavlinkMessage {
718    shape: OwnedMessageDescriptor,
719    payload: Vec<u8>,
720}
721
722impl PamojaMavlinkMessage {
723    /// Wraps a message the engine decoded, for another module in this crate to hand back.
724    pub(crate) fn from_typed(shape: &MessageDescriptor<'static>, payload: Vec<u8>) -> *mut Self {
725        Box::into_raw(Box::new(Self {
726            shape: OwnedMessageDescriptor::from_descriptor(shape),
727            payload,
728        }))
729    }
730}
731
732/// Runs a read against a message, writing the result through `out`.
733unsafe fn get<T: Copy>(
734    message: *const PamojaMavlinkMessage,
735    name: *const c_char,
736    out: *mut T,
737    query: impl FnOnce(&DynamicMessage<'_>, &str) -> MavlinkResult<T>,
738) -> PamojaStatus {
739    let Some(message) = message.as_ref() else {
740        set_last_error("message must not be null".to_owned());
741        return PamojaStatus::InvalidArgument;
742    };
743    if out.is_null() {
744        set_last_error("the output pointer must not be null".to_owned());
745        return PamojaStatus::InvalidArgument;
746    }
747    let Some(name) = read_str(name, "field") else {
748        return PamojaStatus::InvalidArgument;
749    };
750    let result = message.shape.with_descriptor(|shape| {
751        let dynamic = DynamicMessage::decode(shape, &message.payload)?;
752        query(&dynamic, name)
753    });
754    match result {
755        Ok(value) => {
756            *out = value;
757            PamojaStatus::Ok
758        }
759        Err(error) => status_of(error),
760    }
761}
762
763/// Runs a write against a message, keeping its bytes unchanged if the write fails.
764unsafe fn set(
765    message: *mut PamojaMavlinkMessage,
766    name: *const c_char,
767    step: impl FnOnce(&mut DynamicMessage<'_>, &str) -> MavlinkResult<()>,
768) -> PamojaStatus {
769    let Some(message) = message.as_mut() else {
770        set_last_error("message must not be null".to_owned());
771        return PamojaStatus::InvalidArgument;
772    };
773    let Some(name) = read_str(name, "field") else {
774        return PamojaStatus::InvalidArgument;
775    };
776    let payload = std::mem::take(&mut message.payload);
777    let result = message.shape.with_descriptor(|shape| {
778        let mut dynamic = DynamicMessage::decode(shape, &payload)?;
779        step(&mut dynamic, name)?;
780        Ok(dynamic.payload().to_vec())
781    });
782    match result {
783        Ok(updated) => {
784            message.payload = updated;
785            PamojaStatus::Ok
786        }
787        Err(error) => {
788            message.payload = payload;
789            status_of(error)
790        }
791    }
792}
793
794/// Creates a message with every field zero.
795///
796/// # Arguments
797///
798/// * `schema` - the shape of the message to build.
799/// * `out_message` - set to a new message handle on success, which the caller releases
800///   with [`pamoja_mavlink_message_free`].
801///
802/// # Returns
803///
804/// [`PamojaStatus::Ok`] on success.
805///
806/// # Errors
807///
808/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null or the shape does
809/// not fit a MAVLink payload.
810///
811/// # Safety
812///
813/// `schema` must be a live schema handle, and `out_message` must point at writable storage
814/// for one pointer.
815#[no_mangle]
816pub unsafe extern "C" fn pamoja_mavlink_message_new(
817    schema: *const PamojaMavlinkSchema,
818    out_message: *mut *mut PamojaMavlinkMessage,
819) -> PamojaStatus {
820    let Some(schema) = schema.as_ref() else {
821        set_last_error("schema must not be null".to_owned());
822        return PamojaStatus::InvalidArgument;
823    };
824    if out_message.is_null() {
825        set_last_error("out_message must not be null".to_owned());
826        return PamojaStatus::InvalidArgument;
827    }
828    let built = schema.shape.with_descriptor(|shape| {
829        DynamicMessage::new(shape).map(|message| message.payload().to_vec())
830    });
831    match built {
832        Ok(payload) => {
833            *out_message = Box::into_raw(Box::new(PamojaMavlinkMessage {
834                shape: schema.shape.clone(),
835                payload,
836            }));
837            PamojaStatus::Ok
838        }
839        Err(error) => status_of(error),
840    }
841}
842
843/// Reads a message out of a frame payload.
844///
845/// A payload shorter than the shape is zero-extended, as MAVLink 2 truncation requires, so
846/// a frame from a peer that trimmed trailing zeros or predates an extension field decodes.
847///
848/// # Arguments
849///
850/// * `schema` - the shape to read the payload as.
851/// * `payload` - the frame payload.
852/// * `payload_len` - the payload length in bytes.
853/// * `out_message` - set to a new message handle on success, which the caller releases
854///   with [`pamoja_mavlink_message_free`].
855///
856/// # Returns
857///
858/// [`PamojaStatus::Ok`] on success.
859///
860/// # Errors
861///
862/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, and
863/// [`PamojaStatus::Codec`] if the payload is longer than the shape describes.
864///
865/// # Safety
866///
867/// `schema` must be a live schema handle, `payload` must point at `payload_len` readable
868/// bytes, and `out_message` must point at writable storage for one pointer.
869#[no_mangle]
870pub unsafe extern "C" fn pamoja_mavlink_message_decode(
871    schema: *const PamojaMavlinkSchema,
872    payload: *const u8,
873    payload_len: usize,
874    out_message: *mut *mut PamojaMavlinkMessage,
875) -> PamojaStatus {
876    let Some(schema) = schema.as_ref() else {
877        set_last_error("schema must not be null".to_owned());
878        return PamojaStatus::InvalidArgument;
879    };
880    if out_message.is_null() {
881        set_last_error("out_message must not be null".to_owned());
882        return PamojaStatus::InvalidArgument;
883    }
884    let payload = match read_bytes(payload, payload_len) {
885        Ok(payload) => payload,
886        Err(status) => return status,
887    };
888    let decoded = schema.shape.with_descriptor(|shape| {
889        DynamicMessage::decode(shape, &payload).map(|message| message.payload().to_vec())
890    });
891    match decoded {
892        Ok(payload) => {
893            *out_message = Box::into_raw(Box::new(PamojaMavlinkMessage {
894                shape: schema.shape.clone(),
895                payload,
896            }));
897            PamojaStatus::Ok
898        }
899        Err(error) => status_of(error),
900    }
901}
902
903/// Returns a pointer to a message's payload bytes.
904///
905/// The pointer borrows the message and stays valid until it is written to or released.
906///
907/// # Arguments
908///
909/// * `message` - the message to read.
910/// * `out_len` - set to the payload length in bytes.
911///
912/// # Returns
913///
914/// A pointer to the bytes, or null if either pointer is null.
915///
916/// # Safety
917///
918/// `message` must be a live message handle, and `out_len` must point at writable storage
919/// for one length.
920#[no_mangle]
921pub unsafe extern "C" fn pamoja_mavlink_message_payload(
922    message: *const PamojaMavlinkMessage,
923    out_len: *mut usize,
924) -> *const u8 {
925    let Some(message) = message.as_ref() else {
926        return std::ptr::null();
927    };
928    if out_len.is_null() {
929        return std::ptr::null();
930    }
931    *out_len = message.payload.len();
932    message.payload.as_ptr()
933}
934
935/// Builds a v2 frame carrying a message.
936///
937/// # Arguments
938///
939/// * `message` - the message to send.
940/// * `header` - the addressing fields to stamp on the frame.
941/// * `out_frame` - set to a new frame handle on success, which the caller releases with
942///   `pamoja_mavlink_frame_free`.
943///
944/// # Returns
945///
946/// [`PamojaStatus::Ok`] on success.
947///
948/// # Errors
949///
950/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null or the message does
951/// not fit a frame.
952///
953/// # Safety
954///
955/// `message` must be a live message handle, and `out_frame` must point at writable storage
956/// for one pointer.
957#[no_mangle]
958pub unsafe extern "C" fn pamoja_mavlink_message_to_frame(
959    message: *const PamojaMavlinkMessage,
960    header: PamojaMavlinkHeader,
961    out_frame: *mut *mut PamojaMavlinkFrame,
962) -> PamojaStatus {
963    let Some(message) = message.as_ref() else {
964        set_last_error("message must not be null".to_owned());
965        return PamojaStatus::InvalidArgument;
966    };
967    if out_frame.is_null() {
968        set_last_error("out_frame must not be null".to_owned());
969        return PamojaStatus::InvalidArgument;
970    }
971    let built = message.shape.with_descriptor(|shape| {
972        DynamicMessage::decode(shape, &message.payload)?.to_frame(Header::from(header))
973    });
974    match built {
975        Ok(frame) => {
976            *out_frame = PamojaMavlinkFrame::into_handle(frame);
977            PamojaStatus::Ok
978        }
979        Err(error) => status_of(error),
980    }
981}
982
983/// Releases a message.
984///
985/// # Arguments
986///
987/// * `message` - the handle to release; null is ignored.
988///
989/// # Safety
990///
991/// `message` must have come from [`pamoja_mavlink_message_new`] or
992/// [`pamoja_mavlink_message_decode`] and must not be used afterwards.
993#[no_mangle]
994pub unsafe extern "C" fn pamoja_mavlink_message_free(message: *mut PamojaMavlinkMessage) {
995    if !message.is_null() {
996        drop(Box::from_raw(message));
997    }
998}
999
1000/// Reads a field as a signed integer.
1001///
1002/// Any integer field reads this way, whatever its width or sign.
1003///
1004/// # Arguments
1005///
1006/// * `message` - the message to read.
1007/// * `field` - the field name.
1008/// * `index` - the element to read, or `0` for a scalar field.
1009/// * `out_value` - set to the value on success.
1010///
1011/// # Returns
1012///
1013/// [`PamojaStatus::Ok`] on success.
1014///
1015/// # Errors
1016///
1017/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1018/// field, the element is past the end of an array, the field is floating-point, or a
1019/// `uint64_t` value is above the signed range.
1020///
1021/// # Safety
1022///
1023/// `message` must be a live message handle, `field` a null-terminated C string, and
1024/// `out_value` must point at writable storage for one 64-bit integer.
1025#[no_mangle]
1026pub unsafe extern "C" fn pamoja_mavlink_message_get_int(
1027    message: *const PamojaMavlinkMessage,
1028    field: *const c_char,
1029    index: usize,
1030    out_value: *mut i64,
1031) -> PamojaStatus {
1032    get(message, field, out_value, |dynamic, name| {
1033        dynamic.get_int(name, index)
1034    })
1035}
1036
1037/// Reads a field as an unsigned integer.
1038///
1039/// Any integer field reads this way, whatever its width or sign.
1040///
1041/// # Arguments
1042///
1043/// * `message` - the message to read.
1044/// * `field` - the field name.
1045/// * `index` - the element to read, or `0` for a scalar field.
1046/// * `out_value` - set to the value on success.
1047///
1048/// # Returns
1049///
1050/// [`PamojaStatus::Ok`] on success.
1051///
1052/// # Errors
1053///
1054/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1055/// field, the element is past the end of an array, the field is floating-point, or the
1056/// value is negative.
1057///
1058/// # Safety
1059///
1060/// `message` must be a live message handle, `field` a null-terminated C string, and
1061/// `out_value` must point at writable storage for one 64-bit integer.
1062#[no_mangle]
1063pub unsafe extern "C" fn pamoja_mavlink_message_get_uint(
1064    message: *const PamojaMavlinkMessage,
1065    field: *const c_char,
1066    index: usize,
1067    out_value: *mut u64,
1068) -> PamojaStatus {
1069    get(message, field, out_value, |dynamic, name| {
1070        dynamic.get_uint(name, index)
1071    })
1072}
1073
1074/// Reads a floating-point field.
1075///
1076/// # Arguments
1077///
1078/// * `message` - the message to read.
1079/// * `field` - the field name.
1080/// * `index` - the element to read, or `0` for a scalar field.
1081/// * `out_value` - set to the value on success, widened from `float` where needed.
1082///
1083/// # Returns
1084///
1085/// [`PamojaStatus::Ok`] on success.
1086///
1087/// # Errors
1088///
1089/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1090/// field, the element is past the end of an array, or the field is an integer.
1091///
1092/// # Safety
1093///
1094/// `message` must be a live message handle, `field` a null-terminated C string, and
1095/// `out_value` must point at writable storage for one double.
1096#[no_mangle]
1097pub unsafe extern "C" fn pamoja_mavlink_message_get_float(
1098    message: *const PamojaMavlinkMessage,
1099    field: *const c_char,
1100    index: usize,
1101    out_value: *mut f64,
1102) -> PamojaStatus {
1103    get(message, field, out_value, |dynamic, name| {
1104        dynamic.get_float(name, index)
1105    })
1106}
1107
1108/// Writes a signed integer into a field.
1109///
1110/// # Arguments
1111///
1112/// * `message` - the message to write.
1113/// * `field` - the field name.
1114/// * `index` - the element to write, or `0` for a scalar field.
1115/// * `value` - the value to store.
1116///
1117/// # Returns
1118///
1119/// [`PamojaStatus::Ok`] on success.
1120///
1121/// # Errors
1122///
1123/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1124/// field, the element is past the end of an array, the field is floating-point, or the
1125/// value does not fit the field's type.
1126///
1127/// # Safety
1128///
1129/// `message` must be a live message handle and `field` a null-terminated C string.
1130#[no_mangle]
1131pub unsafe extern "C" fn pamoja_mavlink_message_set_int(
1132    message: *mut PamojaMavlinkMessage,
1133    field: *const c_char,
1134    index: usize,
1135    value: i64,
1136) -> PamojaStatus {
1137    set(message, field, |dynamic, name| {
1138        dynamic.set_int(name, index, value)
1139    })
1140}
1141
1142/// Writes an unsigned integer into a field.
1143///
1144/// # Arguments
1145///
1146/// * `message` - the message to write.
1147/// * `field` - the field name.
1148/// * `index` - the element to write, or `0` for a scalar field.
1149/// * `value` - the value to store.
1150///
1151/// # Returns
1152///
1153/// [`PamojaStatus::Ok`] on success.
1154///
1155/// # Errors
1156///
1157/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1158/// field, the element is past the end of an array, the field is floating-point, or the
1159/// value does not fit the field's type.
1160///
1161/// # Safety
1162///
1163/// `message` must be a live message handle and `field` a null-terminated C string.
1164#[no_mangle]
1165pub unsafe extern "C" fn pamoja_mavlink_message_set_uint(
1166    message: *mut PamojaMavlinkMessage,
1167    field: *const c_char,
1168    index: usize,
1169    value: u64,
1170) -> PamojaStatus {
1171    set(message, field, |dynamic, name| {
1172        dynamic.set_uint(name, index, value)
1173    })
1174}
1175
1176/// Writes a floating-point field.
1177///
1178/// # Arguments
1179///
1180/// * `message` - the message to write.
1181/// * `field` - the field name.
1182/// * `index` - the element to write, or `0` for a scalar field.
1183/// * `value` - the value to store, narrowed to `float` where the field is one.
1184///
1185/// # Returns
1186///
1187/// [`PamojaStatus::Ok`] on success.
1188///
1189/// # Errors
1190///
1191/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1192/// field, the element is past the end of an array, or the field is an integer.
1193///
1194/// # Safety
1195///
1196/// `message` must be a live message handle and `field` a null-terminated C string.
1197#[no_mangle]
1198pub unsafe extern "C" fn pamoja_mavlink_message_set_float(
1199    message: *mut PamojaMavlinkMessage,
1200    field: *const c_char,
1201    index: usize,
1202    value: f64,
1203) -> PamojaStatus {
1204    set(message, field, |dynamic, name| {
1205        dynamic.set_float(name, index, value)
1206    })
1207}
1208
1209/// Reads a field as a double, whatever its type.
1210///
1211/// This is the reading a host language with one numeric type needs. An integer field wider
1212/// than 53 bits can exceed what a double holds exactly, so read those with
1213/// [`pamoja_mavlink_message_get_int`] or [`pamoja_mavlink_message_get_uint`] where the
1214/// exact value matters.
1215///
1216/// # Arguments
1217///
1218/// * `message` - the message to read.
1219/// * `field` - the field name.
1220/// * `index` - the element to read, or `0` for a scalar field.
1221/// * `out_value` - set to the value on success.
1222///
1223/// # Returns
1224///
1225/// [`PamojaStatus::Ok`] on success.
1226///
1227/// # Errors
1228///
1229/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1230/// field, or the element is past the end of an array.
1231///
1232/// # Safety
1233///
1234/// `message` must be a live message handle, `field` a null-terminated C string, and
1235/// `out_value` must point at writable storage for one double.
1236#[no_mangle]
1237pub unsafe extern "C" fn pamoja_mavlink_message_get_number(
1238    message: *const PamojaMavlinkMessage,
1239    field: *const c_char,
1240    index: usize,
1241    out_value: *mut f64,
1242) -> PamojaStatus {
1243    get(message, field, out_value, |dynamic, name| {
1244        dynamic.get_number(name, index)
1245    })
1246}
1247
1248/// Writes a double into a field, converting it to the field's type.
1249///
1250/// This is the writing a host language with one numeric type needs. A value bound for an
1251/// integer field must be a whole number within that field's range, so a fractional or
1252/// oversized value is refused rather than silently truncated.
1253///
1254/// # Arguments
1255///
1256/// * `message` - the message to write.
1257/// * `field` - the field name.
1258/// * `index` - the element to write, or `0` for a scalar field.
1259/// * `value` - the value to store.
1260///
1261/// # Returns
1262///
1263/// [`PamojaStatus::Ok`] on success.
1264///
1265/// # Errors
1266///
1267/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1268/// field, the element is past the end of an array, or an integer field is given a value
1269/// that is fractional, infinite, not a number, or outside the range its width holds.
1270///
1271/// # Safety
1272///
1273/// `message` must be a live message handle and `field` a null-terminated C string.
1274#[no_mangle]
1275pub unsafe extern "C" fn pamoja_mavlink_message_set_number(
1276    message: *mut PamojaMavlinkMessage,
1277    field: *const c_char,
1278    index: usize,
1279    value: f64,
1280) -> PamojaStatus {
1281    set(message, field, |dynamic, name| {
1282        dynamic.set_number(name, index, value)
1283    })
1284}
1285
1286/// Copies the raw bytes of a byte-wide array field out.
1287///
1288/// This is how a `char` array carrying text is read: the bytes come back padded with zeros,
1289/// and the caller stops at the first one.
1290///
1291/// # Arguments
1292///
1293/// * `message` - the message to read.
1294/// * `field` - the field name.
1295/// * `out_bytes` - the destination, which must hold at least the field's length.
1296/// * `out_bytes_len` - the space available at `out_bytes`.
1297///
1298/// # Returns
1299///
1300/// [`PamojaStatus::Ok`] on success.
1301///
1302/// # Errors
1303///
1304/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1305/// field, the field is not a byte-wide array, or the destination is too small.
1306///
1307/// # Safety
1308///
1309/// `message` must be a live message handle, `field` a null-terminated C string, and
1310/// `out_bytes` must point at `out_bytes_len` writable bytes.
1311#[no_mangle]
1312pub unsafe extern "C" fn pamoja_mavlink_message_get_bytes(
1313    message: *const PamojaMavlinkMessage,
1314    field: *const c_char,
1315    out_bytes: *mut u8,
1316    out_bytes_len: usize,
1317) -> PamojaStatus {
1318    let Some(message) = message.as_ref() else {
1319        set_last_error("message must not be null".to_owned());
1320        return PamojaStatus::InvalidArgument;
1321    };
1322    if out_bytes.is_null() {
1323        set_last_error("out_bytes must not be null".to_owned());
1324        return PamojaStatus::InvalidArgument;
1325    }
1326    let Some(name) = read_str(field, "field") else {
1327        return PamojaStatus::InvalidArgument;
1328    };
1329    let out = std::slice::from_raw_parts_mut(out_bytes, out_bytes_len);
1330    let read = message.shape.with_descriptor(|shape| {
1331        DynamicMessage::decode(shape, &message.payload)?
1332            .get_bytes(name, out)
1333            .map(|_| ())
1334    });
1335    match read {
1336        Ok(()) => PamojaStatus::Ok,
1337        Err(error) => status_of(error),
1338    }
1339}
1340
1341/// Writes the raw bytes of a byte-wide array field, zero-padding the rest.
1342///
1343/// This is how a `char` array carrying text is written: pass the text's bytes and the field
1344/// is padded to its declared length.
1345///
1346/// # Arguments
1347///
1348/// * `message` - the message to write.
1349/// * `field` - the field name.
1350/// * `bytes` - the bytes to store, at most the field's declared length.
1351/// * `bytes_len` - the number of bytes to store.
1352///
1353/// # Returns
1354///
1355/// [`PamojaStatus::Ok`] on success.
1356///
1357/// # Errors
1358///
1359/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the message has no such
1360/// field, the field is not a byte-wide array, or the bytes are longer than the field.
1361///
1362/// # Safety
1363///
1364/// `message` must be a live message handle, `field` a null-terminated C string, and
1365/// `bytes` must point at `bytes_len` readable bytes.
1366#[no_mangle]
1367pub unsafe extern "C" fn pamoja_mavlink_message_set_bytes(
1368    message: *mut PamojaMavlinkMessage,
1369    field: *const c_char,
1370    bytes: *const u8,
1371    bytes_len: usize,
1372) -> PamojaStatus {
1373    let bytes = match read_bytes(bytes, bytes_len) {
1374        Ok(bytes) => bytes,
1375        Err(status) => return status,
1376    };
1377    set(message, field, |dynamic, name| {
1378        dynamic.set_bytes(name, &bytes)
1379    })
1380}
1381
1382#[cfg(test)]
1383mod tests {
1384    use super::*;
1385    use std::ffi::CString;
1386
1387    unsafe fn schema_for(name: &str) -> *mut PamojaMavlinkSchema {
1388        let mut schema = std::ptr::null_mut();
1389        let wanted = CString::new(name).unwrap();
1390        assert_eq!(
1391            pamoja_mavlink_schema_for_name(wanted.as_ptr(), &mut schema),
1392            PamojaStatus::Ok
1393        );
1394        schema
1395    }
1396
1397    #[test]
1398    fn a_typed_message_is_described_and_filled_in_by_name() {
1399        unsafe {
1400            let schema = schema_for("HEARTBEAT");
1401            assert_eq!(pamoja_mavlink_schema_id(schema), 0);
1402            assert_eq!(pamoja_mavlink_schema_crc_extra(schema), 50);
1403            assert_eq!(pamoja_mavlink_schema_wire_len(schema), 9);
1404            assert_eq!(pamoja_mavlink_schema_field_count(schema), 6);
1405
1406            // The 32-bit field leads, as wire order puts it.
1407            let mut field = std::mem::zeroed::<PamojaMavlinkFieldInfo>();
1408            assert_eq!(
1409                pamoja_mavlink_schema_field(schema, 0, &mut field),
1410                PamojaStatus::Ok
1411            );
1412            assert_eq!(
1413                std::ffi::CStr::from_ptr(field.name).to_str().unwrap(),
1414                "custom_mode"
1415            );
1416            assert_eq!(
1417                std::ffi::CStr::from_ptr(field.type_name).to_str().unwrap(),
1418                "uint32_t"
1419            );
1420            assert_eq!(field.field_type, PAMOJA_MAVLINK_FIELD_UINT32);
1421            assert_eq!(field.offset, 0);
1422            assert_eq!(field.extension, 0);
1423
1424            let mut message = std::ptr::null_mut();
1425            assert_eq!(
1426                pamoja_mavlink_message_new(schema, &mut message),
1427                PamojaStatus::Ok
1428            );
1429            let kind = CString::new("type").unwrap();
1430            assert_eq!(
1431                pamoja_mavlink_message_set_uint(message, kind.as_ptr(), 0, 18),
1432                PamojaStatus::Ok
1433            );
1434            let mut value = 0u64;
1435            assert_eq!(
1436                pamoja_mavlink_message_get_uint(message, kind.as_ptr(), 0, &mut value),
1437                PamojaStatus::Ok
1438            );
1439            assert_eq!(value, 18);
1440
1441            // Out of range for a uint8_t, and the message keeps its old bytes.
1442            assert_eq!(
1443                pamoja_mavlink_message_set_uint(message, kind.as_ptr(), 0, 300),
1444                PamojaStatus::InvalidArgument
1445            );
1446            assert_eq!(
1447                pamoja_mavlink_message_get_uint(message, kind.as_ptr(), 0, &mut value),
1448                PamojaStatus::Ok
1449            );
1450            assert_eq!(value, 18);
1451
1452            let mut len = 0;
1453            let payload = pamoja_mavlink_message_payload(message, &mut len);
1454            assert_eq!(std::slice::from_raw_parts(payload, len)[4], 18);
1455
1456            pamoja_mavlink_message_free(message);
1457            pamoja_mavlink_schema_free(schema);
1458        }
1459    }
1460
1461    #[test]
1462    fn a_private_message_is_described_and_carried_like_any_other() {
1463        unsafe {
1464            let name = CString::new("BATTERY_CELLS").unwrap();
1465            let builder = pamoja_mavlink_schema_builder_new(50_000, name.as_ptr());
1466            assert!(!builder.is_null());
1467
1468            let cells = CString::new("cell_mv").unwrap();
1469            let pack = CString::new("pack_id").unwrap();
1470            let uptime = CString::new("uptime_ms").unwrap();
1471            assert_eq!(
1472                pamoja_mavlink_schema_builder_field(
1473                    builder,
1474                    cells.as_ptr(),
1475                    PAMOJA_MAVLINK_FIELD_UINT16,
1476                    6
1477                ),
1478                PamojaStatus::Ok
1479            );
1480            assert_eq!(
1481                pamoja_mavlink_schema_builder_field(
1482                    builder,
1483                    pack.as_ptr(),
1484                    PAMOJA_MAVLINK_FIELD_UINT8,
1485                    0
1486                ),
1487                PamojaStatus::Ok
1488            );
1489            assert_eq!(
1490                pamoja_mavlink_schema_builder_field(
1491                    builder,
1492                    uptime.as_ptr(),
1493                    PAMOJA_MAVLINK_FIELD_UINT32,
1494                    0
1495                ),
1496                PamojaStatus::Ok
1497            );
1498
1499            let mut schema = std::ptr::null_mut();
1500            assert_eq!(
1501                pamoja_mavlink_schema_builder_build(builder, &mut schema),
1502                PamojaStatus::Ok
1503            );
1504            assert_eq!(pamoja_mavlink_schema_wire_len(schema), 17);
1505
1506            // Wire order pulls the 32-bit field to the front.
1507            let mut field = std::mem::zeroed::<PamojaMavlinkFieldInfo>();
1508            assert_eq!(
1509                pamoja_mavlink_schema_field(schema, 0, &mut field),
1510                PamojaStatus::Ok
1511            );
1512            assert_eq!(
1513                std::ffi::CStr::from_ptr(field.name).to_str().unwrap(),
1514                "uptime_ms"
1515            );
1516
1517            let mut message = std::ptr::null_mut();
1518            assert_eq!(
1519                pamoja_mavlink_message_new(schema, &mut message),
1520                PamojaStatus::Ok
1521            );
1522            assert_eq!(
1523                pamoja_mavlink_message_set_uint(message, cells.as_ptr(), 3, 4_150),
1524                PamojaStatus::Ok
1525            );
1526            let mut value = 0u64;
1527            assert_eq!(
1528                pamoja_mavlink_message_get_uint(message, cells.as_ptr(), 3, &mut value),
1529                PamojaStatus::Ok
1530            );
1531            assert_eq!(value, 4_150);
1532
1533            pamoja_mavlink_message_free(message);
1534            pamoja_mavlink_schema_free(schema);
1535        }
1536    }
1537
1538    #[test]
1539    fn an_unknown_message_or_field_is_reported_rather_than_guessed() {
1540        unsafe {
1541            let mut schema = std::ptr::null_mut();
1542            assert_eq!(
1543                pamoja_mavlink_schema_for_id(50_000, &mut schema),
1544                PamojaStatus::Unsupported
1545            );
1546
1547            let heartbeat = schema_for("HEARTBEAT");
1548            let mut message = std::ptr::null_mut();
1549            assert_eq!(
1550                pamoja_mavlink_message_new(heartbeat, &mut message),
1551                PamojaStatus::Ok
1552            );
1553            let missing = CString::new("throttle").unwrap();
1554            let mut value = 0u64;
1555            assert_eq!(
1556                pamoja_mavlink_message_get_uint(message, missing.as_ptr(), 0, &mut value),
1557                PamojaStatus::InvalidArgument
1558            );
1559            pamoja_mavlink_message_free(message);
1560            pamoja_mavlink_schema_free(heartbeat);
1561        }
1562    }
1563}