Skip to main content

mx_remote_ffi/
remote.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! The client handle: making one, running it, and asking it what it has found.
5
6use std::ffi::c_char;
7use std::ffi::c_void;
8use std::net::Ipv4Addr;
9use std::path::PathBuf;
10use std::str::FromStr;
11use std::sync::Arc;
12
13use mx_remote::{Config, DeviceUid, EventHandler, Remote};
14
15use crate::abi::{
16    fail, from_io, from_send, guard, mxr_bay_uid_t, mxr_result_t, mxr_uid_t, opt_str, put_str,
17    req_str,
18};
19use crate::events::{mxr_callbacks_t, Bridge};
20
21/// Bytes an IPv4 address needs when written as text, the terminator included.
22pub const MXR_IP_STRING_LEN: usize = 16;
23
24/// A running client. Opaque: everything about it is reached through the
25/// functions below.
26pub struct mxr_remote_t {
27    pub(crate) remote: Remote,
28}
29
30/// How a client finds the network.
31///
32/// Zeroing the whole struct asks for the default: multicast discovery on
33/// whichever interface the host picks, which is the right answer on a machine
34/// with one network and an arbitrary one on any other.
35#[repr(C)]
36pub struct mxr_config_t {
37    /// Where to send. Null means the multicast group, or the interface's
38    /// broadcast address when `broadcast` is set.
39    pub target_ip: *const c_char,
40    /// UDP port. Zero means the default for the selected mode.
41    pub port: u16,
42    /// Use broadcast rather than multicast.
43    pub broadcast: bool,
44    /// Selects the interface by address, as text. Null lets the host choose.
45    ///
46    /// It decides both which interface frames leave by and which one they are
47    /// accepted on. Getting it wrong on a multi-homed host fails one-sidedly:
48    /// devices are still discovered, because their broadcasts arrive by any
49    /// route, while every request this client sends leaves by the wrong
50    /// interface and is never answered.
51    pub local_ip: *const c_char,
52    /// Selects the interface by name, taking precedence over `local_ip`.
53    ///
54    /// An interface with no address of its own - a tagged VLAN - can be named
55    /// only this way, and only on Linux.
56    pub interface: *const c_char,
57    /// The name this client advertises to devices. Null means a default.
58    pub name: *const c_char,
59    /// This client's identifier, in the form `mxr_uid_to_string()` writes.
60    ///
61    /// Null loads it from `uid_path`, generating and storing one on first run.
62    /// It must be stable across restarts, or every peer counts each run as a
63    /// new client.
64    ///
65    /// `mxr_uid_to_string()`: crate::mxr_uid_to_string
66    pub uid: *const c_char,
67    /// Where the identifier is kept. Null means `.mxr-uid` in the user's home
68    /// directory.
69    pub uid_path: *const c_char,
70}
71
72/// Reads an address argument, where null means "not set".
73unsafe fn opt_ip(ptr: *const c_char, what: &str) -> Result<Option<Ipv4Addr>, mxr_result_t> {
74    // SAFETY: the caller guarantees a NUL-terminated string or null.
75    let text = match unsafe { opt_str(ptr) }? {
76        Some(s) => s,
77        None => return Ok(None),
78    };
79    match Ipv4Addr::from_str(text) {
80        Ok(ip) => Ok(Some(ip)),
81        Err(_) => Err(fail(
82            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
83            &format!("{what} is not an IPv4 address: {text:?}"),
84        )),
85    }
86}
87
88/// Builds the core crate's configuration from the caller's.
89///
90/// # Safety
91///
92/// Every non-null pointer in `c` is a NUL-terminated string.
93unsafe fn to_config(c: &mxr_config_t) -> Result<Config, mxr_result_t> {
94    // SAFETY: the caller guarantees NUL-terminated strings or null.
95    let (target_ip, local_ip) = unsafe {
96        (
97            opt_ip(c.target_ip, "target_ip")?,
98            opt_ip(c.local_ip, "local_ip")?,
99        )
100    };
101    // SAFETY: as above.
102    let (interface, name, uid_text, uid_path) = unsafe {
103        (
104            opt_str(c.interface)?,
105            opt_str(c.name)?,
106            opt_str(c.uid)?,
107            opt_str(c.uid_path)?,
108        )
109    };
110    let uid = match uid_text {
111        Some(text) => match DeviceUid::from_str(text) {
112            Ok(uid) => Some(uid),
113            Err(e) => return Err(fail(mxr_result_t::MXR_ERR_INVALID_ARGUMENT, &e.to_string())),
114        },
115        None => None,
116    };
117
118    // Config is non_exhaustive, so it is filled in rather than built: a field
119    // added upstream keeps whatever default it is given there.
120    let mut config = Config::default();
121    config.target_ip = target_ip;
122    // Zero is not a port a socket can be sent to, so it is how the caller says
123    // nothing rather than a value to pass on.
124    config.port = (c.port != 0).then_some(c.port);
125    config.local_ip = local_ip;
126    config.interface = interface.map(str::to_owned);
127    config.broadcast = c.broadcast;
128    config.name = name.map(str::to_owned);
129    config.uid = uid;
130    config.uid_path = uid_path.map(PathBuf::from);
131    Ok(config)
132}
133
134/// Creates a client, without opening a socket yet.
135///
136/// `config` may be null for the defaults. `callbacks` may be null, and so may
137/// any member of it: an event with no function pointer is dropped. `userdata`
138/// is passed back to every callback and is never examined.
139///
140/// Returns null on failure, with the reason in
141/// `mxr_last_error()`. The client must be released with
142/// `mxr_remote_free()`.
143///
144/// # Safety
145///
146/// `config` and `callbacks` are null or point at initialised structs that
147/// outlive the call, and every string in them is NUL-terminated. `userdata`
148/// must remain valid, and safe to use from the library's own threads, until
149/// `mxr_remote_free()` returns.
150#[no_mangle]
151pub unsafe extern "C" fn mxr_remote_new(
152    config: *const mxr_config_t,
153    callbacks: *const mxr_callbacks_t,
154    userdata: *mut c_void,
155) -> *mut mxr_remote_t {
156    guard(std::ptr::null_mut(), || {
157        // SAFETY: the caller guarantees an initialised struct or null.
158        let config = match unsafe { config.as_ref() } {
159            // SAFETY: its string members carry the same guarantee.
160            Some(c) => match unsafe { to_config(c) } {
161                Ok(c) => c,
162                Err(_) => return std::ptr::null_mut(),
163            },
164            None => Config::default(),
165        };
166        // SAFETY: the caller guarantees an initialised table or null, and
167        // guarantees userdata outlives the client.
168        let handler: Arc<dyn EventHandler> = match unsafe { callbacks.as_ref() } {
169            Some(table) => Arc::new(Bridge::new(table, userdata)),
170            None => Arc::new(()),
171        };
172        match Remote::new(config, handler) {
173            Ok(remote) => Box::into_raw(Box::new(mxr_remote_t { remote })),
174            Err(e) => {
175                fail(mxr_result_t::MXR_ERR_IO, &e.to_string());
176                std::ptr::null_mut()
177            }
178        }
179    })
180}
181
182/// Opens the socket and starts the receive and timer threads.
183///
184/// # Safety
185///
186/// `remote` is null or a handle from `mxr_remote_new()` that has not been
187/// freed.
188#[no_mangle]
189pub unsafe extern "C" fn mxr_remote_start(remote: *const mxr_remote_t) -> mxr_result_t {
190    // SAFETY: the caller guarantees a live handle or null.
191    let handle = unsafe { remote.as_ref() };
192    with(handle, |r| from_io(r.remote.start()))
193}
194
195/// Stops the threads and closes the socket. Idempotent.
196///
197/// A handle that has been closed can be freed but not restarted.
198///
199/// # Safety
200///
201/// `remote` is null or a handle from `mxr_remote_new()` that has not been
202/// freed.
203#[no_mangle]
204pub unsafe extern "C" fn mxr_remote_close(remote: *const mxr_remote_t) {
205    // SAFETY: the caller guarantees a live handle or null.
206    let handle = unsafe { remote.as_ref() };
207    with(handle, |r| {
208        r.remote.close();
209        mxr_result_t::MXR_OK
210    });
211}
212
213/// Closes the client and releases it. Null is ignored.
214///
215/// This waits for the receive and timer threads to finish, so a callback
216/// running when it is called returns before it does - which means calling it
217/// from inside a callback would deadlock.
218///
219/// # Safety
220///
221/// `remote` is null or a handle from `mxr_remote_new()` that has not already
222/// been freed, and no other thread is using it.
223#[no_mangle]
224pub unsafe extern "C" fn mxr_remote_free(remote: *mut mxr_remote_t) {
225    guard((), || {
226        if remote.is_null() {
227            return;
228        }
229        // SAFETY: the caller guarantees a handle from mxr_remote_new that has
230        // not been freed, so this reclaims the box that call leaked.
231        drop(unsafe { Box::from_raw(remote) });
232    });
233}
234
235/// Runs `body` on a handle, rejecting null and catching a panic.
236///
237/// It takes a reference rather than the raw pointer so that the one unsafe
238/// step - deciding that the caller's pointer is a live handle - stays at the
239/// entry point, where the caller's guarantee is written down.
240pub(crate) fn with(
241    remote: Option<&mxr_remote_t>,
242    body: impl FnOnce(&mxr_remote_t) -> mxr_result_t,
243) -> mxr_result_t {
244    guard(mxr_result_t::MXR_ERR_PANIC, || match remote {
245        Some(r) => body(r),
246        None => fail(
247            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
248            "the client handle is null",
249        ),
250    })
251}
252
253/// Copies a list into a caller's array and reports how long the list is.
254///
255/// The return value is the full length whether or not it fitted, so a caller
256/// can size a buffer by calling once with `cap` zero. `out` may be null only
257/// when `cap` is zero.
258///
259/// # Safety
260///
261/// `out` is null or points at `cap` writable elements.
262unsafe fn copy_out<T: Copy, U: Copy + Into<T>>(items: &[U], out: *mut T, cap: usize) -> usize {
263    if !out.is_null() {
264        // SAFETY: the caller guarantees cap writable elements at out.
265        let dst = unsafe { std::slice::from_raw_parts_mut(out, cap) };
266        for (slot, item) in dst.iter_mut().zip(items) {
267            *slot = (*item).into();
268        }
269    }
270    items.len()
271}
272
273/// Writes this client's own identifier.
274///
275/// # Safety
276///
277/// `remote` is null or a live handle, and `out` points at a writable
278/// [`mxr_uid_t`].
279#[no_mangle]
280pub unsafe extern "C" fn mxr_remote_uid(
281    remote: *const mxr_remote_t,
282    out: *mut mxr_uid_t,
283) -> mxr_result_t {
284    // SAFETY: the caller guarantees a live handle or null.
285    let handle = unsafe { remote.as_ref() };
286    with(handle, |r| {
287        if out.is_null() {
288            return fail(
289                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
290                "uid output pointer is null",
291            );
292        }
293        // SAFETY: checked non-null just above.
294        unsafe { *out = r.remote.uid().into() };
295        mxr_result_t::MXR_OK
296    })
297}
298
299/// Writes the name this client advertises.
300///
301/// # Safety
302///
303/// `remote` is null or a live handle, and `out` points at `cap` writable bytes.
304#[no_mangle]
305pub unsafe extern "C" fn mxr_remote_name(
306    remote: *const mxr_remote_t,
307    out: *mut c_char,
308    cap: usize,
309) -> mxr_result_t {
310    // SAFETY: the caller guarantees a live handle or null.
311    let handle = unsafe { remote.as_ref() };
312    with(handle, |r| {
313        if out.is_null() || cap == 0 {
314            return fail(
315                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
316                "name buffer is null or empty",
317            );
318        }
319        // SAFETY: the caller guarantees cap writable bytes at out.
320        put_str(
321            unsafe { std::slice::from_raw_parts_mut(out, cap) },
322            r.remote.name(),
323        );
324        mxr_result_t::MXR_OK
325    })
326}
327
328/// Writes the address this client sends to.
329///
330/// Fails with `MXR_ERR_NOT_CONNECTED` before `mxr_remote_start()`. `ip` needs
331/// [`MXR_IP_STRING_LEN`] bytes; either output may be null to skip it.
332///
333/// # Safety
334///
335/// `remote` is null or a live handle, `ip` is null or points at `cap` writable
336/// bytes, and `port` is null or points at a writable `uint16_t`.
337#[no_mangle]
338pub unsafe extern "C" fn mxr_remote_target(
339    remote: *const mxr_remote_t,
340    ip: *mut c_char,
341    cap: usize,
342    port: *mut u16,
343) -> mxr_result_t {
344    // SAFETY: the caller guarantees a live handle or null.
345    let handle = unsafe { remote.as_ref() };
346    with(handle, |r| {
347        let target = match r.remote.target() {
348            Some(t) => t,
349            None => {
350                return fail(
351                    mxr_result_t::MXR_ERR_NOT_CONNECTED,
352                    "the client has no socket",
353                )
354            }
355        };
356        if !ip.is_null() {
357            if cap < MXR_IP_STRING_LEN {
358                return fail(
359                    mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
360                    "address buffer is shorter than MXR_IP_STRING_LEN",
361                );
362            }
363            // SAFETY: the caller guarantees cap writable bytes at ip.
364            let dst = unsafe { std::slice::from_raw_parts_mut(ip, cap) };
365            put_str(dst, &target.ip().to_string());
366        }
367        if !port.is_null() {
368            // SAFETY: the caller guarantees a writable uint16_t at port.
369            unsafe { *port = target.port() };
370        }
371        mxr_result_t::MXR_OK
372    })
373}
374
375/// Writes how many frames from other senders have parsed since
376/// `mxr_remote_start()`.
377///
378/// It separates a mesh with nothing on it from an interface nothing is on: a
379/// client that has discovered no device but is counting frames is hearing
380/// traffic it cannot get answers from, which on a multi-homed host is what a
381/// wrong `mxr_config_t::local_ip` looks like. Frames this client sent are not
382/// counted, because the host loops its own multicast back whichever interface
383/// was selected.
384///
385/// # Safety
386///
387/// `remote` is null or a live handle, and `out` points at a writable
388/// `uint64_t`.
389#[no_mangle]
390pub unsafe extern "C" fn mxr_frames_received(
391    remote: *const mxr_remote_t,
392    out: *mut u64,
393) -> mxr_result_t {
394    // SAFETY: the caller guarantees a live handle or null.
395    let handle = unsafe { remote.as_ref() };
396    with(handle, |r| {
397        if out.is_null() {
398            return fail(
399                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
400                "frame count output pointer is null",
401            );
402        }
403        // SAFETY: checked non-null just above.
404        unsafe { *out = r.remote.frames_received() };
405        mxr_result_t::MXR_OK
406    })
407}
408
409/// Writes every device heard from, and returns how many there are.
410///
411/// Returns the full count even when it exceeds `cap`, so calling with `cap`
412/// zero sizes the buffer. Returns zero on a null handle.
413///
414/// # Safety
415///
416/// `remote` is null or a live handle, and `out` is null or points at `cap`
417/// writable [`mxr_uid_t`].
418#[no_mangle]
419pub unsafe extern "C" fn mxr_devices(
420    remote: *const mxr_remote_t,
421    out: *mut mxr_uid_t,
422    cap: usize,
423) -> usize {
424    guard(0, || {
425        // SAFETY: the caller guarantees a live handle or null.
426        let Some(r) = (unsafe { remote.as_ref() }) else {
427            fail(
428                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
429                "the client handle is null",
430            );
431            return 0;
432        };
433        // SAFETY: the caller guarantees cap writable elements at out.
434        unsafe { copy_out(&r.remote.devices(), out, cap) }
435    })
436}
437
438/// Finds a device by its serial number.
439///
440/// # Safety
441///
442/// `remote` is null or a live handle, `serial` is a NUL-terminated string, and
443/// `out` points at a writable [`mxr_uid_t`].
444#[no_mangle]
445pub unsafe extern "C" fn mxr_device_by_serial(
446    remote: *const mxr_remote_t,
447    serial: *const c_char,
448    out: *mut mxr_uid_t,
449) -> mxr_result_t {
450    // SAFETY: the caller guarantees a live handle or null.
451    let handle = unsafe { remote.as_ref() };
452    with(handle, |r| {
453        // SAFETY: the caller guarantees a NUL-terminated string.
454        let serial = match unsafe { req_str(serial) } {
455            Ok(s) => s,
456            Err(code) => return code,
457        };
458        // SAFETY: the caller guarantees a writable mxr_uid_t.
459        unsafe { write_uid(r.remote.device_by_serial(serial), out, "serial", serial) }
460    })
461}
462
463/// Finds a device by serial number, name or identifier, in that order.
464///
465/// # Safety
466///
467/// `remote` is null or a live handle, `name` is a NUL-terminated string, and
468/// `out` points at a writable [`mxr_uid_t`].
469#[no_mangle]
470pub unsafe extern "C" fn mxr_resolve_device(
471    remote: *const mxr_remote_t,
472    name: *const c_char,
473    out: *mut mxr_uid_t,
474) -> mxr_result_t {
475    // SAFETY: the caller guarantees a live handle or null.
476    let handle = unsafe { remote.as_ref() };
477    with(handle, |r| {
478        // SAFETY: the caller guarantees a NUL-terminated string.
479        let name = match unsafe { req_str(name) } {
480            Ok(s) => s,
481            Err(code) => return code,
482        };
483        // SAFETY: the caller guarantees a writable mxr_uid_t.
484        unsafe { write_uid(r.remote.resolve_device(name), out, "device", name) }
485    })
486}
487
488/// Writes a device lookup's answer, or reports that it found nothing.
489///
490/// # Safety
491///
492/// `out` points at a writable [`mxr_uid_t`].
493unsafe fn write_uid(
494    found: Option<mx_remote::DeviceUid>,
495    out: *mut mxr_uid_t,
496    what: &str,
497    key: &str,
498) -> mxr_result_t {
499    if out.is_null() {
500        return fail(
501            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
502            "uid output pointer is null",
503        );
504    }
505    match found {
506        Some(uid) => {
507            // SAFETY: checked non-null just above.
508            unsafe { *out = uid.into() };
509            mxr_result_t::MXR_OK
510        }
511        None => fail(
512            mxr_result_t::MXR_ERR_NOT_FOUND,
513            &format!("no device with {what} {key:?}"),
514        ),
515    }
516}
517
518/// Finds a bay on a device by the name the device gives its port.
519///
520/// # Safety
521///
522/// `remote` is null or a live handle, `port_name` is a NUL-terminated string,
523/// and `out` points at a writable [`mxr_bay_uid_t`].
524#[no_mangle]
525pub unsafe extern "C" fn mxr_bay_by_name(
526    remote: *const mxr_remote_t,
527    device: mxr_uid_t,
528    port_name: *const c_char,
529    out: *mut mxr_bay_uid_t,
530) -> mxr_result_t {
531    // SAFETY: the caller guarantees a live handle or null.
532    let handle = unsafe { remote.as_ref() };
533    with(handle, |r| {
534        // SAFETY: the caller guarantees a NUL-terminated string.
535        let port_name = match unsafe { req_str(port_name) } {
536            Ok(s) => s,
537            Err(code) => return code,
538        };
539        let found = r.remote.bay_by_name(device.into(), port_name);
540        // SAFETY: the caller guarantees a writable mxr_bay_uid_t.
541        unsafe { write_bay(found, out, &format!("no bay named {port_name:?}")) }
542    })
543}
544
545/// Finds the source bay advertising a multicast group.
546///
547/// `audio` picks which of the bay's two streams the address is matched
548/// against.
549///
550/// # Safety
551///
552/// `remote` is null or a live handle, `ip` is a NUL-terminated string, and
553/// `out` points at a writable [`mxr_bay_uid_t`].
554#[no_mangle]
555pub unsafe extern "C" fn mxr_bay_by_stream_ip(
556    remote: *const mxr_remote_t,
557    ip: *const c_char,
558    audio: bool,
559    out: *mut mxr_bay_uid_t,
560) -> mxr_result_t {
561    // SAFETY: the caller guarantees a live handle or null.
562    let handle = unsafe { remote.as_ref() };
563    with(handle, |r| {
564        // SAFETY: the caller guarantees a NUL-terminated string or null.
565        let ip = match unsafe { opt_ip(ip, "ip") } {
566            Ok(Some(ip)) => ip,
567            Ok(None) => {
568                return fail(
569                    mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
570                    "a required string argument was null",
571                )
572            }
573            Err(code) => return code,
574        };
575        let found = r.remote.bay_by_stream_ip(ip, audio);
576        // SAFETY: the caller guarantees a writable mxr_bay_uid_t.
577        unsafe { write_bay(found, out, &format!("no bay streams to {ip}")) }
578    })
579}
580
581/// Writes a bay lookup's answer, or reports that it found nothing.
582///
583/// # Safety
584///
585/// `out` points at a writable [`mxr_bay_uid_t`].
586unsafe fn write_bay(
587    found: Option<mx_remote::BayUid>,
588    out: *mut mxr_bay_uid_t,
589    message: &str,
590) -> mxr_result_t {
591    if out.is_null() {
592        return fail(
593            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
594            "bay output pointer is null",
595        );
596    }
597    match found {
598        Some(bay) => {
599            // SAFETY: checked non-null just above.
600            unsafe { *out = bay.into() };
601            mxr_result_t::MXR_OK
602        }
603        None => fail(mxr_result_t::MXR_ERR_NOT_FOUND, message),
604    }
605}
606
607/// Reopens the socket on a different interface, or in the other mode.
608///
609/// `local_ip` may be null to let the host choose again.
610///
611/// # Safety
612///
613/// `remote` is null or a live handle, and `local_ip` is null or a
614/// NUL-terminated string.
615#[no_mangle]
616pub unsafe extern "C" fn mxr_remote_update_config(
617    remote: *const mxr_remote_t,
618    local_ip: *const c_char,
619    broadcast: bool,
620) -> mxr_result_t {
621    // SAFETY: the caller guarantees a live handle or null.
622    let handle = unsafe { remote.as_ref() };
623    with(handle, |r| {
624        // SAFETY: the caller guarantees a NUL-terminated string or null.
625        let ip = match unsafe { opt_ip(local_ip, "local_ip") } {
626            Ok(ip) => ip,
627            Err(code) => return code,
628        };
629        from_io(r.remote.update_config(ip, broadcast))
630    })
631}
632
633/// Asks every device on the network to announce itself.
634///
635/// # Safety
636///
637/// `remote` is null or a live handle.
638#[no_mangle]
639pub unsafe extern "C" fn mxr_discover(remote: *const mxr_remote_t) -> mxr_result_t {
640    // SAFETY: the caller guarantees a live handle or null.
641    let handle = unsafe { remote.as_ref() };
642    with(handle, |r| from_send(r.remote.discover()))
643}