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 every device heard from, and returns how many there are.
376///
377/// Returns the full count even when it exceeds `cap`, so calling with `cap`
378/// zero sizes the buffer. Returns zero on a null handle.
379///
380/// # Safety
381///
382/// `remote` is null or a live handle, and `out` is null or points at `cap`
383/// writable [`mxr_uid_t`].
384#[no_mangle]
385pub unsafe extern "C" fn mxr_devices(
386    remote: *const mxr_remote_t,
387    out: *mut mxr_uid_t,
388    cap: usize,
389) -> usize {
390    guard(0, || {
391        // SAFETY: the caller guarantees a live handle or null.
392        let Some(r) = (unsafe { remote.as_ref() }) else {
393            fail(
394                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
395                "the client handle is null",
396            );
397            return 0;
398        };
399        // SAFETY: the caller guarantees cap writable elements at out.
400        unsafe { copy_out(&r.remote.devices(), out, cap) }
401    })
402}
403
404/// Finds a device by its serial number.
405///
406/// # Safety
407///
408/// `remote` is null or a live handle, `serial` is a NUL-terminated string, and
409/// `out` points at a writable [`mxr_uid_t`].
410#[no_mangle]
411pub unsafe extern "C" fn mxr_device_by_serial(
412    remote: *const mxr_remote_t,
413    serial: *const c_char,
414    out: *mut mxr_uid_t,
415) -> mxr_result_t {
416    // SAFETY: the caller guarantees a live handle or null.
417    let handle = unsafe { remote.as_ref() };
418    with(handle, |r| {
419        // SAFETY: the caller guarantees a NUL-terminated string.
420        let serial = match unsafe { req_str(serial) } {
421            Ok(s) => s,
422            Err(code) => return code,
423        };
424        // SAFETY: the caller guarantees a writable mxr_uid_t.
425        unsafe { write_uid(r.remote.device_by_serial(serial), out, "serial", serial) }
426    })
427}
428
429/// Finds a device by serial number, name or identifier, in that order.
430///
431/// # Safety
432///
433/// `remote` is null or a live handle, `name` is a NUL-terminated string, and
434/// `out` points at a writable [`mxr_uid_t`].
435#[no_mangle]
436pub unsafe extern "C" fn mxr_resolve_device(
437    remote: *const mxr_remote_t,
438    name: *const c_char,
439    out: *mut mxr_uid_t,
440) -> mxr_result_t {
441    // SAFETY: the caller guarantees a live handle or null.
442    let handle = unsafe { remote.as_ref() };
443    with(handle, |r| {
444        // SAFETY: the caller guarantees a NUL-terminated string.
445        let name = match unsafe { req_str(name) } {
446            Ok(s) => s,
447            Err(code) => return code,
448        };
449        // SAFETY: the caller guarantees a writable mxr_uid_t.
450        unsafe { write_uid(r.remote.resolve_device(name), out, "device", name) }
451    })
452}
453
454/// Writes a device lookup's answer, or reports that it found nothing.
455///
456/// # Safety
457///
458/// `out` points at a writable [`mxr_uid_t`].
459unsafe fn write_uid(
460    found: Option<mx_remote::DeviceUid>,
461    out: *mut mxr_uid_t,
462    what: &str,
463    key: &str,
464) -> mxr_result_t {
465    if out.is_null() {
466        return fail(
467            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
468            "uid output pointer is null",
469        );
470    }
471    match found {
472        Some(uid) => {
473            // SAFETY: checked non-null just above.
474            unsafe { *out = uid.into() };
475            mxr_result_t::MXR_OK
476        }
477        None => fail(
478            mxr_result_t::MXR_ERR_NOT_FOUND,
479            &format!("no device with {what} {key:?}"),
480        ),
481    }
482}
483
484/// Finds a bay on a device by the name the device gives its port.
485///
486/// # Safety
487///
488/// `remote` is null or a live handle, `port_name` is a NUL-terminated string,
489/// and `out` points at a writable [`mxr_bay_uid_t`].
490#[no_mangle]
491pub unsafe extern "C" fn mxr_bay_by_name(
492    remote: *const mxr_remote_t,
493    device: mxr_uid_t,
494    port_name: *const c_char,
495    out: *mut mxr_bay_uid_t,
496) -> mxr_result_t {
497    // SAFETY: the caller guarantees a live handle or null.
498    let handle = unsafe { remote.as_ref() };
499    with(handle, |r| {
500        // SAFETY: the caller guarantees a NUL-terminated string.
501        let port_name = match unsafe { req_str(port_name) } {
502            Ok(s) => s,
503            Err(code) => return code,
504        };
505        let found = r.remote.bay_by_name(device.into(), port_name);
506        // SAFETY: the caller guarantees a writable mxr_bay_uid_t.
507        unsafe { write_bay(found, out, &format!("no bay named {port_name:?}")) }
508    })
509}
510
511/// Finds the source bay advertising a multicast group.
512///
513/// `audio` picks which of the bay's two streams the address is matched
514/// against.
515///
516/// # Safety
517///
518/// `remote` is null or a live handle, `ip` is a NUL-terminated string, and
519/// `out` points at a writable [`mxr_bay_uid_t`].
520#[no_mangle]
521pub unsafe extern "C" fn mxr_bay_by_stream_ip(
522    remote: *const mxr_remote_t,
523    ip: *const c_char,
524    audio: bool,
525    out: *mut mxr_bay_uid_t,
526) -> mxr_result_t {
527    // SAFETY: the caller guarantees a live handle or null.
528    let handle = unsafe { remote.as_ref() };
529    with(handle, |r| {
530        // SAFETY: the caller guarantees a NUL-terminated string or null.
531        let ip = match unsafe { opt_ip(ip, "ip") } {
532            Ok(Some(ip)) => ip,
533            Ok(None) => {
534                return fail(
535                    mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
536                    "a required string argument was null",
537                )
538            }
539            Err(code) => return code,
540        };
541        let found = r.remote.bay_by_stream_ip(ip, audio);
542        // SAFETY: the caller guarantees a writable mxr_bay_uid_t.
543        unsafe { write_bay(found, out, &format!("no bay streams to {ip}")) }
544    })
545}
546
547/// Writes a bay lookup's answer, or reports that it found nothing.
548///
549/// # Safety
550///
551/// `out` points at a writable [`mxr_bay_uid_t`].
552unsafe fn write_bay(
553    found: Option<mx_remote::BayUid>,
554    out: *mut mxr_bay_uid_t,
555    message: &str,
556) -> mxr_result_t {
557    if out.is_null() {
558        return fail(
559            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
560            "bay output pointer is null",
561        );
562    }
563    match found {
564        Some(bay) => {
565            // SAFETY: checked non-null just above.
566            unsafe { *out = bay.into() };
567            mxr_result_t::MXR_OK
568        }
569        None => fail(mxr_result_t::MXR_ERR_NOT_FOUND, message),
570    }
571}
572
573/// Reopens the socket on a different interface, or in the other mode.
574///
575/// `local_ip` may be null to let the host choose again.
576///
577/// # Safety
578///
579/// `remote` is null or a live handle, and `local_ip` is null or a
580/// NUL-terminated string.
581#[no_mangle]
582pub unsafe extern "C" fn mxr_remote_update_config(
583    remote: *const mxr_remote_t,
584    local_ip: *const c_char,
585    broadcast: bool,
586) -> mxr_result_t {
587    // SAFETY: the caller guarantees a live handle or null.
588    let handle = unsafe { remote.as_ref() };
589    with(handle, |r| {
590        // SAFETY: the caller guarantees a NUL-terminated string or null.
591        let ip = match unsafe { opt_ip(local_ip, "local_ip") } {
592            Ok(ip) => ip,
593            Err(code) => return code,
594        };
595        from_io(r.remote.update_config(ip, broadcast))
596    })
597}
598
599/// Asks every device on the network to announce itself.
600///
601/// # Safety
602///
603/// `remote` is null or a live handle.
604#[no_mangle]
605pub unsafe extern "C" fn mxr_discover(remote: *const mxr_remote_t) -> mxr_result_t {
606    // SAFETY: the caller guarantees a live handle or null.
607    let handle = unsafe { remote.as_ref() };
608    with(handle, |r| from_send(r.remote.discover()))
609}