//! Safe native replacements for the CLR delegate types in the Vivox API. use crate::Error; use libremetaverse_types::compat::{Object, SocketException}; use std::any::Any; use std::fmt; use std::sync::Arc; type AsyncCallback = Box; type AsyncResult = Box; macro_rules! callback_type { ($name:ident ( $($argument:ident : $type:ty),* $(,)? )) => { #[derive(Clone)] pub struct $name { handler: Arc Result<(), Error> + Send + Sync>, } impl fmt::Debug for $name { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct(stringify!($name)) .finish_non_exhaustive() } } impl $name { /// Creates a native callback without exposing CLR method pointers. pub fn from_handler(handler: F) -> Self where F: Fn($($type),*) -> Result<(), Error> + Send + Sync + 'static, { Self { handler: Arc::new(handler) } } /// Rehydrates a callback previously stored in a mapped `Object`. /// A non-zero CLR method pointer cannot be invoked safely in native /// Rust and is therefore rejected explicitly. pub fn new(object: Object, method: isize) -> Result { if method != 0 { return Err(Error::Argument); } object.downcast_ref::().cloned().ok_or(Error::Argument) } pub fn invoke(&self, $($argument: $type),*) -> Result<(), Error> { (self.handler)($($argument),*) } pub fn begin_invoke( &self, $($argument: $type,)* callback: AsyncCallback, object: Object, ) -> Result { self.invoke($($argument),*)?; callback(&object); Ok(Box::new(())) } pub fn end_invoke(&self, result: AsyncResult) -> Result<(), Error> { result.downcast::<()>().map(|_| ()).map_err(|_| Error::Argument) } } }; } callback_type!(TCPPipeOnDisconnectedCallback(se: SocketException)); callback_type!(TCPPipeOnReceiveLineCallback(line: String)); callback_type!(VoiceGatewayDaemonConnectedCallback()); callback_type!(VoiceGatewayDaemonCouldntConnectCallback()); callback_type!(VoiceGatewayDaemonCouldntRunCallback()); callback_type!(VoiceGatewayDaemonDisconnectedCallback()); callback_type!(VoiceGatewayDaemonExitedCallback()); callback_type!(VoiceGatewayDaemonRunningCallback()); callback_type!(VoiceGatewayVoiceConnectionChangeCallback( state: crate::VoiceGatewayConnectionState, )); callback_type!(VoiceGatewayVoiceMicTestCallback(level: f32)); callback_type!(VoiceManagerAuxAudioPropertiesCallback(cookie: i32, energy: f32)); callback_type!(VoiceManagerBasicActionCallback( cookie: i32, status_code: i32, status_string: String, )); callback_type!(VoiceManagerConnectorCreatedCallback( cookie: i32, status_code: i32, status_string: String, connector_handle: String, )); callback_type!(VoiceManagerDevicesCallback( cookie: i32, status_code: i32, status_string: String, current_device: String, )); callback_type!(VoiceManagerLoginCallback( cookie: i32, status_code: i32, status_string: String, account_handle: String, )); callback_type!(VoiceManagerLoginStateChangeCallback( cookie: i32, account_handle: String, status_code: i32, status_string: String, state: i32, )); callback_type!(VoiceManagerNewSessionCallback( cookie: i32, account_handle: String, event_session_handle: String, state: i32, name_string: String, uri_string: String, )); callback_type!(VoiceManagerParcelVoiceInfoCallback( region_name: String, local_id: i32, channel_uri: Option, )); callback_type!(VoiceManagerParticipantPropertiesCallback( cookie: i32, uri_string: String, status_code: i32, status_string: String, is_locally_muted: bool, is_moderator_muted: bool, is_speaking: bool, volume: i32, energy: f32, )); callback_type!(VoiceManagerParticipantStateChangeCallback( cookie: i32, uri_string: String, status_code: i32, status_string: String, state: i32, name_string: String, display_name_string: String, participant_type: i32, )); callback_type!(VoiceManagerProvisionAccountCallback( username: String, password: String, )); callback_type!(VoiceManagerSessionCreatedCallback( cookie: i32, status_code: i32, status_string: String, session_handle: String, )); callback_type!(VoiceManagerSessionStateChangeCallback( cookie: i32, uri_string: String, status_code: i32, status_string: String, event_session_handle: String, state: i32, is_channel: bool, name_string: String, )); #[cfg(test)] mod tests { use super::*; use std::sync::atomic::{AtomicI32, Ordering}; #[test] fn callback_invocation_and_apm_completion_execute_once() { let total = Arc::new(AtomicI32::new(0)); let observed = Arc::clone(&total); let callback = VoiceManagerAuxAudioPropertiesCallback::from_handler(move |cookie, energy| { observed.fetch_add(cookie + energy as i32, Ordering::SeqCst); Ok(()) }); callback.invoke(2, 3.0).unwrap(); let completion_count = Arc::new(AtomicI32::new(0)); let completed = Arc::clone(&completion_count); let result = callback .begin_invoke( 4, 5.0, Box::new(move |_| { completed.fetch_add(1, Ordering::SeqCst); }), Object::Undefined, ) .unwrap(); callback.end_invoke(result).unwrap(); assert_eq!(total.load(Ordering::SeqCst), 14); assert_eq!(completion_count.load(Ordering::SeqCst), 1); } #[test] fn callback_round_trips_through_mapped_object_without_raw_pointer() { let callback = TCPPipeOnReceiveLineCallback::from_handler(|_| Ok(())); let restored = TCPPipeOnReceiveLineCallback::new(Object::opaque(callback), 0).unwrap(); restored.invoke("line".into()).unwrap(); assert!(TCPPipeOnReceiveLineCallback::new(Object::Undefined, 0).is_err()); assert!(TCPPipeOnReceiveLineCallback::new(Object::Undefined, 1).is_err()); } }