Skip to main content

moq/
ffi.rs

1use std::{
2	cell::RefCell,
3	ffi::{CString, c_char, c_void},
4	sync::LazyLock,
5};
6
7use url::Url;
8
9use crate::{Error, Id, moq_protocol_error};
10
11/// A callback receiving a positive handle/value, zero on clean completion, or a negative error.
12#[allow(non_camel_case_types)]
13pub type moq_status_callback = Option<extern "C" fn(user_data: *mut c_void, code: i32)>;
14
15pub static RUNTIME: LazyLock<tokio::runtime::Handle> = LazyLock::new(|| {
16	let runtime = tokio::runtime::Builder::new_current_thread()
17		.enable_all()
18		.build()
19		.unwrap();
20	let handle = runtime.handle().clone();
21
22	std::thread::Builder::new()
23		.name("libmoq".into())
24		.spawn(move || {
25			runtime.block_on(std::future::pending::<()>());
26		})
27		.expect("failed to spawn runtime thread");
28
29	handle
30});
31
32/// Runs the provided function in the runtime context.
33/// Additionally, we convert the return code to a C-compatible return value.
34///
35/// Callers run concurrently: entering a handle only sets a thread-local, so
36/// nothing here is shared between threads. Tokio's requirement that the guards
37/// be dropped in LIFO order is per-thread too, and this one lives and dies in
38/// this frame, so a nested call nests rather than crosses.
39pub fn enter<C: ReturnCode, F: FnOnce() -> C>(f: F) -> i32 {
40	let _guard = RUNTIME.enter();
41
42	match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
43		Ok(ret) => {
44			record_error(&ret);
45			ret.code()
46		}
47		Err(_) => {
48			record_error(&Error::Panic);
49			Error::Panic.code()
50		}
51	}
52}
53
54/// Wrapper for C callback functions with user data.
55///
56/// Stores a function pointer and user data pointer to call C callbacks
57/// from async Rust code.
58#[derive(Clone, Copy)]
59pub struct OnStatus {
60	user_data: *mut c_void,
61	on_status: extern "C" fn(user_data: *mut c_void, code: i32),
62}
63
64impl OnStatus {
65	/// Create a new callback wrapper from a C function pointer.
66	///
67	/// # Safety
68	/// - The caller must ensure user_data remains valid for the callback's lifetime.
69	/// - The callback function pointer must be valid if provided.
70	pub unsafe fn new(user_data: *mut c_void, on_status: moq_status_callback) -> Result<Self, Error> {
71		Ok(Self {
72			user_data,
73			on_status: on_status.ok_or(Error::InvalidPointer)?,
74		})
75	}
76
77	/// Invoke the callback with a result code.
78	///
79	/// We record the reason before invoking the callback (on the same thread)
80	/// so a callback receiving a negative code can read `moq_error()` for it.
81	pub fn call<C: ReturnCode>(&self, ret: C) {
82		record_error(&ret);
83		let code = ret.code();
84		(self.on_status)(self.user_data, code);
85	}
86}
87
88unsafe impl Send for OnStatus {}
89
90/// Types that can be converted to C-compatible return codes.
91pub trait ReturnCode {
92	/// Convert to an i32 status code.
93	fn code(&self) -> i32;
94
95	/// The error this carries, if any, so the boundary can record its reason
96	/// for `moq_error`. Defaults to none for non-fallible return types.
97	fn error(&self) -> Option<&Error> {
98		None
99	}
100}
101
102impl ReturnCode for () {
103	fn code(&self) -> i32 {
104		0
105	}
106}
107
108impl ReturnCode for i32 {
109	fn code(&self) -> i32 {
110		*self
111	}
112}
113
114impl ReturnCode for Result<i32, Error> {
115	fn code(&self) -> i32 {
116		match self {
117			Ok(code) if *code < 0 => Error::InvalidCode.code(),
118			Ok(code) => *code,
119			Err(e) => e.code(),
120		}
121	}
122
123	fn error(&self) -> Option<&Error> {
124		self.as_ref().err()
125	}
126}
127
128impl ReturnCode for Result<usize, Error> {
129	fn code(&self) -> i32 {
130		match self {
131			Ok(code) => i32::try_from(*code).unwrap_or_else(|_| Error::InvalidCode.code()),
132			Err(e) => e.code(),
133		}
134	}
135
136	fn error(&self) -> Option<&Error> {
137		self.as_ref().err()
138	}
139}
140
141impl ReturnCode for Result<Id, Error> {
142	fn code(&self) -> i32 {
143		match self {
144			Ok(id) => i32::from(*id),
145			Err(e) => e.code(),
146		}
147	}
148
149	fn error(&self) -> Option<&Error> {
150		self.as_ref().err()
151	}
152}
153
154impl ReturnCode for Result<(), Error> {
155	fn code(&self) -> i32 {
156		match self {
157			Ok(()) => 0,
158			Err(e) => e.code(),
159		}
160	}
161
162	fn error(&self) -> Option<&Error> {
163		self.as_ref().err()
164	}
165}
166
167impl ReturnCode for usize {
168	fn code(&self) -> i32 {
169		i32::try_from(*self).unwrap_or_else(|_| Error::InvalidCode.code())
170	}
171}
172
173impl ReturnCode for Id {
174	fn code(&self) -> i32 {
175		i32::from(*self)
176	}
177}
178
179struct LastError {
180	message: CString,
181	protocol: Option<moq_protocol_error>,
182}
183
184thread_local! {
185	/// Reason for the most recent error returned on this thread. FFI functions
186	/// hand back only a numeric code, so we stash the human-readable message
187	/// (and protocol details, when the failure is a session or stream code)
188	/// here for `moq_error` / `moq_error_protocol` to retrieve.
189	static LAST_ERROR: RefCell<Option<LastError>> = const { RefCell::new(None) };
190}
191
192/// Record the reason for an error return into this thread's `moq_error` slot.
193///
194/// Called at the FFI boundary (sync return and callback dispatch) right before
195/// the numeric code is produced, so the conversion in `code()` stays pure.
196fn record_error<C: ReturnCode>(ret: &C) {
197	let Some(err) = ret.error() else { return };
198	// CString::new fails only on an interior NUL, which our messages never
199	// contain; skip storing rather than truncating if it ever happens.
200	if let Ok(msg) = CString::new(err.to_string()) {
201		LAST_ERROR.with(|cell| {
202			*cell.borrow_mut() = Some(LastError {
203				message: msg,
204				protocol: err.protocol(),
205			});
206		});
207	}
208}
209
210/// Pointer to this thread's last error message, or null if none was recorded.
211///
212/// The pointer is valid until the next libmoq call on the same thread.
213pub fn last_error_ptr() -> *const c_char {
214	LAST_ERROR.with(|cell| {
215		cell.borrow()
216			.as_ref()
217			.map_or(std::ptr::null(), |err| err.message.as_ptr())
218	})
219}
220
221/// Copy this thread's last protocol error into `out`.
222///
223/// Returns true when the last error was a protocol failure and `out` was written.
224pub fn last_protocol(out: &mut moq_protocol_error) -> bool {
225	LAST_ERROR.with(|cell| match cell.borrow().as_ref().and_then(|err| err.protocol) {
226		Some(protocol) => {
227			*out = protocol;
228			true
229		}
230		None => false,
231	})
232}
233
234/// Parse an i32 handle into an Id.
235pub fn parse_id(id: u32) -> Result<Id, Error> {
236	Id::try_from(id)
237}
238
239/// Parse an optional i32 handle (0 = None) into an Option<Id>.
240pub fn parse_id_optional(id: u32) -> Result<Option<Id>, Error> {
241	match id {
242		0 => Ok(None),
243		id => Ok(Some(parse_id(id)?)),
244	}
245}
246
247/// Parse a C string pointer into a Url.
248pub fn parse_url(url: *const c_char, url_len: usize) -> Result<Url, Error> {
249	let url = unsafe { parse_str(url, url_len)? };
250	Ok(Url::parse(url)?)
251}
252
253/// Parse a C string pointer into a &str.
254///
255/// Returns an empty string if the pointer is null.
256///
257/// # Safety
258/// The caller must ensure that cstr is valid for 'a.
259pub unsafe fn parse_str<'a>(cstr: *const c_char, cstr_len: usize) -> Result<&'a str, Error> {
260	let slice = unsafe { parse_slice(cstr.cast::<u8>(), cstr_len)? };
261	let string = std::str::from_utf8(slice)?;
262	Ok(string)
263}
264
265/// Parse an optional C string, where a NULL or empty value means "unset".
266///
267/// Config setters use this so one function both sets and clears a knob, rather than
268/// needing a paired `moq_client_clear_*` for every optional field.
269///
270/// # Safety
271/// The caller must ensure that cstr is valid for 'a.
272pub unsafe fn parse_str_optional<'a>(cstr: *const c_char, cstr_len: usize) -> Result<Option<&'a str>, Error> {
273	if cstr.is_null() {
274		return Ok(None);
275	}
276
277	let string = unsafe { parse_str(cstr, cstr_len)? };
278	Ok((!string.is_empty()).then_some(string))
279}
280
281/// Parse a C array of [`crate::moq_string`] into owned strings.
282///
283/// A NULL array is only valid when `count` is zero, which yields an empty list.
284///
285/// # Safety
286/// The caller must ensure that items is valid for count elements, and that each
287/// element points to its own length in bytes.
288pub unsafe fn parse_strings(items: *const crate::moq_string, count: usize) -> Result<Vec<String>, Error> {
289	if items.is_null() {
290		if count == 0 {
291			return Ok(Vec::new());
292		}
293
294		return Err(Error::InvalidPointer);
295	}
296
297	let items = unsafe { std::slice::from_raw_parts(items, count) };
298	items
299		.iter()
300		.map(|item| Ok(unsafe { parse_str(item.data, item.len)? }.to_string()))
301		.collect()
302}
303
304/// Parse a raw pointer and size into a byte slice.
305///
306/// Returns an empty slice if both pointer and size are zero.
307///
308/// # Safety
309/// The caller must ensure that data is valid for 'a.
310pub unsafe fn parse_slice<'a>(data: *const u8, size: usize) -> Result<&'a [u8], Error> {
311	if data.is_null() {
312		if size == 0 {
313			return Ok(&[]);
314		}
315
316		return Err(Error::InvalidPointer);
317	}
318
319	let data = unsafe { std::slice::from_raw_parts(data, size) };
320	Ok(data)
321}