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}