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.