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