Skip to main content

aivdm_ffi/
lib.rs

1//! C FFI bindings for the `aivdm` AIS decoder.
2//!
3//! Exposes a decode-only subset of `aivdm`'s API: decode a single-fragment
4//! `!AIVDM`/`!AIVDO` line into an opaque message handle, then read common
5//! fields (MMSI, position, speed, course, heading, navigational status) off
6//! it. See `include/aivdm.h` for the generated C declarations.
7//!
8//! This crate is a plain (non-`no_std`) library: it targets host
9//! environments linking a shared or static C library, not the embedded
10//! `no_alloc` use case `aivdm` itself supports. Embedded consumers should use
11//! the `aivdm` Rust crate directly.
12
13use std::ffi::{CStr, c_char};
14use std::ptr;
15
16use aivdm::AisMessage;
17
18/// Opaque handle to a decoded AIS message, owned by the caller until freed
19/// with [`aivdm_message_free`].
20pub struct AivdmMessage(AisMessage);
21
22/// Error codes returned by [`aivdm_decode_line`].
23#[repr(u8)]
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum AivdmError {
26    /// No error.
27    Ok = 0,
28    /// The `line` pointer was NULL.
29    NullInput = 1,
30    /// `line` was not valid UTF-8.
31    InvalidUtf8 = 2,
32    /// The NMEA sentence itself was malformed (bad checksum, wrong field
33    /// count, unsupported talker/formatter, invalid armor character).
34    Nmea = 3,
35    /// The sentence is one fragment of a multi-part message; this
36    /// decode-only entry point only handles single-fragment sentences.
37    IncompleteFragment = 4,
38    /// A fragment-reassembly error (not produced by this entry point, which
39    /// does no reassembly, but reserved for API symmetry).
40    Fragment = 5,
41    /// The payload was truncated or otherwise malformed at the bit level.
42    Bit = 6,
43    /// The message type is unknown, or a field held an invalid value.
44    Message = 7,
45}
46
47impl From<aivdm::AisError> for AivdmError {
48    fn from(e: aivdm::AisError) -> Self {
49        match e {
50            aivdm::AisError::Nmea(_) => Self::Nmea,
51            aivdm::AisError::IncompleteFragment => Self::IncompleteFragment,
52            aivdm::AisError::Fragment(_) => Self::Fragment,
53            aivdm::AisError::Bit(_) => Self::Bit,
54            // covers Message(_) and any future non_exhaustive variant
55            _ => Self::Message,
56        }
57    }
58}
59
60/// Decodes a single, complete, single-fragment `!AIVDM`/`!AIVDO` line.
61///
62/// On success, returns a non-NULL handle (free it with
63/// [`aivdm_message_free`] when done) and, if `out_error` is non-NULL, writes
64/// [`AivdmError::Ok`] through it. On failure, returns NULL and, if
65/// `out_error` is non-NULL, writes the reason through it.
66///
67/// # Safety
68/// `line` must be NULL or a pointer to a NUL-terminated C string valid for
69/// reads for the duration of this call. `out_error` must be NULL or a
70/// pointer valid for a single `u8`-sized write.
71#[unsafe(no_mangle)]
72pub unsafe extern "C" fn aivdm_decode_line(
73    line: *const c_char,
74    out_error: *mut AivdmError,
75) -> *mut AivdmMessage {
76    let set_error = |code: AivdmError| {
77        if !out_error.is_null() {
78            // SAFETY: caller guarantees `out_error` is a valid write target or NULL.
79            unsafe {
80                out_error.write(code);
81            }
82        }
83    };
84
85    if line.is_null() {
86        set_error(AivdmError::NullInput);
87        return ptr::null_mut();
88    }
89
90    // SAFETY: caller guarantees `line` is a valid NUL-terminated C string.
91    let Ok(line_str) = unsafe { CStr::from_ptr(line) }.to_str() else {
92        set_error(AivdmError::InvalidUtf8);
93        return ptr::null_mut();
94    };
95
96    match aivdm::decode_line(line_str) {
97        Ok(msg) => {
98            set_error(AivdmError::Ok);
99            Box::into_raw(Box::new(AivdmMessage(msg)))
100        }
101        Err(e) => {
102            set_error(AivdmError::from(e));
103            ptr::null_mut()
104        }
105    }
106}
107
108/// Frees a message handle returned by [`aivdm_decode_line`]. Passing NULL is
109/// a no-op.
110///
111/// # Safety
112/// `msg` must be NULL or a pointer previously returned by
113/// [`aivdm_decode_line`] that has not already been freed.
114#[unsafe(no_mangle)]
115pub unsafe extern "C" fn aivdm_message_free(msg: *mut AivdmMessage) {
116    if !msg.is_null() {
117        // SAFETY: caller guarantees `msg` is a live handle from `aivdm_decode_line`.
118        drop(unsafe { Box::from_raw(msg) });
119    }
120}
121
122/// The ITU-R M.1371 message type number (1-27) of a decoded message.
123///
124/// # Safety
125/// `msg` must be NULL or a valid, non-freed handle from [`aivdm_decode_line`].
126#[unsafe(no_mangle)]
127pub unsafe extern "C" fn aivdm_message_type(msg: *const AivdmMessage) -> u8 {
128    // SAFETY: caller guarantees `msg` is a valid handle or NULL.
129    match unsafe { msg.as_ref() } {
130        Some(msg) => msg.0.message_type(),
131        None => 0,
132    }
133}
134
135/// How many times a repeater has relayed this message (0-3).
136///
137/// # Safety
138/// `msg` must be NULL or a valid, non-freed handle from [`aivdm_decode_line`].
139#[unsafe(no_mangle)]
140pub unsafe extern "C" fn aivdm_message_repeat_indicator(msg: *const AivdmMessage) -> u8 {
141    // SAFETY: caller guarantees `msg` is a valid handle or NULL.
142    match unsafe { msg.as_ref() } {
143        Some(msg) => msg.0.repeat_indicator(),
144        None => 0,
145    }
146}
147
148/// The source station MMSI.
149///
150/// # Safety
151/// `msg` must be NULL or a valid, non-freed handle from [`aivdm_decode_line`].
152#[unsafe(no_mangle)]
153pub unsafe extern "C" fn aivdm_message_mmsi(msg: *const AivdmMessage) -> u32 {
154    // SAFETY: caller guarantees `msg` is a valid handle or NULL.
155    match unsafe { msg.as_ref() } {
156        Some(msg) => msg.0.mmsi().raw(),
157        None => 0,
158    }
159}
160
161/// Reads the position, in decimal degrees, off a message.
162///
163/// Returns `true` and writes `*out_lat`/`*out_lon` if this message type
164/// carries a position and it is marked available; returns `false` (leaving
165/// the outputs untouched) otherwise.
166///
167/// # Safety
168/// `msg` must be NULL or a valid, non-freed handle from
169/// [`aivdm_decode_line`]. `out_lat` and `out_lon` must be valid pointers to
170/// writable `f64`s.
171#[unsafe(no_mangle)]
172pub unsafe extern "C" fn aivdm_message_position(
173    msg: *const AivdmMessage,
174    out_lat: *mut f64,
175    out_lon: *mut f64,
176) -> bool {
177    // SAFETY: caller guarantees `msg` is a valid handle or NULL.
178    let Some(msg) = (unsafe { msg.as_ref() }) else {
179        return false;
180    };
181    let Some(position) = msg.0.position() else {
182        return false;
183    };
184    // SAFETY: caller guarantees `out_lat`/`out_lon` are valid write targets.
185    unsafe {
186        out_lat.write(position.latitude);
187        out_lon.write(position.longitude);
188    }
189    true
190}
191
192/// Reads speed over ground, in knots, off a message.
193///
194/// Returns `true` and writes `*out_sog` if this message type carries a
195/// speed and it is marked available; returns `false` otherwise.
196///
197/// # Safety
198/// `msg` must be NULL or a valid, non-freed handle from
199/// [`aivdm_decode_line`]. `out_sog` must be a valid pointer to a writable `f64`.
200#[unsafe(no_mangle)]
201pub unsafe extern "C" fn aivdm_message_sog_knots(
202    msg: *const AivdmMessage,
203    out_sog: *mut f64,
204) -> bool {
205    // SAFETY: caller guarantees `msg` is a valid handle or NULL.
206    let Some(msg) = (unsafe { msg.as_ref() }) else {
207        return false;
208    };
209    let Some(sog) = msg.0.sog_knots() else {
210        return false;
211    };
212    // SAFETY: caller guarantees `out_sog` is a valid write target.
213    unsafe {
214        out_sog.write(sog);
215    }
216    true
217}
218
219/// Reads course over ground, in degrees, off a message.
220///
221/// Returns `true` and writes `*out_cog` if this message type carries a
222/// course and it is marked available; returns `false` otherwise.
223///
224/// # Safety
225/// `msg` must be NULL or a valid, non-freed handle from
226/// [`aivdm_decode_line`]. `out_cog` must be a valid pointer to a writable `f64`.
227#[unsafe(no_mangle)]
228pub unsafe extern "C" fn aivdm_message_cog_degrees(
229    msg: *const AivdmMessage,
230    out_cog: *mut f64,
231) -> bool {
232    // SAFETY: caller guarantees `msg` is a valid handle or NULL.
233    let Some(msg) = (unsafe { msg.as_ref() }) else {
234        return false;
235    };
236    let Some(cog) = msg.0.cog_degrees() else {
237        return false;
238    };
239    // SAFETY: caller guarantees `out_cog` is a valid write target.
240    unsafe {
241        out_cog.write(cog);
242    }
243    true
244}
245
246/// Reads true heading, in whole degrees (0-359), off a message.
247///
248/// Returns `true` and writes `*out_heading` if this message type carries a
249/// heading and it is marked available; returns `false` otherwise.
250///
251/// # Safety
252/// `msg` must be NULL or a valid, non-freed handle from
253/// [`aivdm_decode_line`]. `out_heading` must be a valid pointer to a writable `u16`.
254#[unsafe(no_mangle)]
255pub unsafe extern "C" fn aivdm_message_heading_degrees(
256    msg: *const AivdmMessage,
257    out_heading: *mut u16,
258) -> bool {
259    // SAFETY: caller guarantees `msg` is a valid handle or NULL.
260    let Some(msg) = (unsafe { msg.as_ref() }) else {
261        return false;
262    };
263    let Some(heading) = msg.0.heading_degrees() else {
264        return false;
265    };
266    // SAFETY: caller guarantees `out_heading` is a valid write target.
267    unsafe {
268        out_heading.write(heading);
269    }
270    true
271}
272
273/// Reads the raw navigational status code (0-15, ITU-R M.1371 Table 45) off
274/// a message.
275///
276/// Returns `true` and writes `*out_status` if this message type carries a
277/// navigational status; returns `false` otherwise.
278///
279/// # Safety
280/// `msg` must be NULL or a valid, non-freed handle from
281/// [`aivdm_decode_line`]. `out_status` must be a valid pointer to a writable `u8`.
282#[unsafe(no_mangle)]
283pub unsafe extern "C" fn aivdm_message_navigation_status(
284    msg: *const AivdmMessage,
285    out_status: *mut u8,
286) -> bool {
287    // SAFETY: caller guarantees `msg` is a valid handle or NULL.
288    let Some(msg) = (unsafe { msg.as_ref() }) else {
289        return false;
290    };
291    let Some(status) = msg.0.navigation_status() else {
292        return false;
293    };
294    let status = status.to_raw();
295    // SAFETY: caller guarantees `out_status` is a valid write target.
296    unsafe {
297        out_status.write(status);
298    }
299    true
300}
301
302/// The `aivdm` crate version, as a NUL-terminated, statically allocated
303/// string (e.g. `"0.1.0"`). Do not free.
304#[unsafe(no_mangle)]
305pub extern "C" fn aivdm_version() -> *const c_char {
306    const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
307    VERSION.as_ptr().cast::<c_char>()
308}
309
310// repeat_indicator/mmsi/position/sog_knots/cog_degrees/heading_degrees/
311// navigation_status are all provided directly by `AisMessage` in the core
312// crate (see `aivdm::message::AisMessage`) and called via
313// `msg.0.<method>()` above, rather than duplicated here.