diff --git a/crates/irsdk/examples/stream_var.rs b/crates/irsdk/examples/stream_var.rs index eb6f763..676806d 100644 --- a/crates/irsdk/examples/stream_var.rs +++ b/crates/irsdk/examples/stream_var.rs @@ -11,9 +11,18 @@ fn main() -> Result<(), Box> { let mut buf = vec![0; client.buf_len()]; let var = client.vars().var(&var_name).expect("unknown var"); - while let Ok(sample) = client.next_sample_into_buf(&mut buf) { - println!("{:?}", sample.read_var(var)); - std::thread::sleep(Duration::from_millis(50)); + loop { + match client.next_sample_into_buf(&mut buf) { + Ok(sample) => { + println!("{:?}", sample.read_var(var)); + std::thread::sleep(Duration::from_millis(50)); + } + Err(err) => { + println!("{err:#?}"); + println!("{err}"); + break; + } + } } Ok(()) diff --git a/crates/irsdk/src/client.rs b/crates/irsdk/src/client.rs index d02dc8b..c6a0160 100644 --- a/crates/irsdk/src/client.rs +++ b/crates/irsdk/src/client.rs @@ -1,39 +1,34 @@ +use crate::win; +use crate::win::{TelemetryMemMap, WindowsError}; use ibt::raw; use ibt::telemetry::{Header, Sample, VarBufInfo, VarHeader, VarSet}; use itertools::Itertools; -use windows::Win32::Foundation::{CloseHandle, HANDLE}; -use windows::Win32::System::Memory::{ - FILE_MAP_READ, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, OpenFileMappingW, UnmapViewOfFile, -}; -use windows::Win32::System::Threading::{ - OpenEventW, SYNCHRONIZATION_SYNCHRONIZE, WaitForSingleObject, -}; -use windows::core::{PCWSTR, w}; - -const MEM_MAP_FILE_NAME: PCWSTR = w!(r"Local\IRSDKMemMapFileName"); -const DATA_VALID_EVENT_NAME: PCWSTR = w!(r"Local\IRSDKDataValidEvent"); -const TIMEOUT_MS: u32 = 1000; -const FILE_NOT_FOUND_CODE: i32 = 0x80070002u32 as i32; +use std::time::Duration; + +const TIMEOUT: Duration = Duration::from_millis(1000); #[derive(Clone, Debug, thiserror::Error)] pub enum IRacingClientError { #[error("Unknown windows error")] - Windows(#[source] windows::core::Error), + Windows(#[source] WindowsError), #[error("iRacing is not running")] - Disconnected(#[source] Option), + Disconnected, #[error(transparent)] RawTelemError(#[from] ibt::RawTelemError), #[error(transparent)] RawConversionError(#[from] ibt::telemetry::RawConversionError), + + #[error(transparent)] + SignalError(#[from] win::SignalError), } -impl From for IRacingClientError { - fn from(err: windows::core::Error) -> Self { - if err.code().0 == FILE_NOT_FOUND_CODE { - Self::Disconnected(Some(err)) +impl From for IRacingClientError { + fn from(err: WindowsError) -> Self { + if err.is_file_not_found() { + Self::Disconnected } else { Self::Windows(err) } @@ -42,9 +37,7 @@ impl From for IRacingClientError { #[derive(Debug)] pub struct IRacingClient { - file_mapping_handle: HANDLE, - mem_map_address: MEMORY_MAPPED_VIEW_ADDRESS, - event_handle: HANDLE, + mem_map: TelemetryMemMap, vars: VarSet, buf_len: usize, @@ -52,55 +45,38 @@ pub struct IRacingClient { impl IRacingClient { pub fn connect() -> Result { - let file_mapping_handle = - unsafe { OpenFileMappingW(FILE_MAP_READ.0, false, MEM_MAP_FILE_NAME)? }; - let mem_map_address = unsafe { MapViewOfFile(file_mapping_handle, FILE_MAP_READ, 0, 0, 0) }; - let event_handle = - unsafe { OpenEventW(SYNCHRONIZATION_SYNCHRONIZE, false, DATA_VALID_EVENT_NAME)? }; - - let mut client = Self { - file_mapping_handle, - mem_map_address, - event_handle, - vars: VarSet::new(vec![]), - buf_len: 0, - }; + let mem_map = TelemetryMemMap::connect()?; + mem_map.wait_for_event_signal(TIMEOUT)?; - let raw_header = client.next_raw_header()?; + let raw_header = unsafe { mem_map.as_raw_header()? }; let header = Header::from_raw(&raw_header)?; - client.buf_len = header.buf_len; // Read the var headers once let vh_offset = raw_header.var_header_offset as usize; let vh_len = raw::VAR_HEADER_SIZE * raw_header.num_vars as usize; - let vh_slice = unsafe { client.slice(vh_offset, vh_len) }; + let vh_slice = unsafe { mem_map.as_slice(vh_offset, vh_len) }; let var_headers = raw::VarHeader::slice_from_fraw_bytes(vh_slice) .iter() .map(VarHeader::from_raw) .collect(); - client.vars = VarSet::new(var_headers); - Ok(client) - } - - unsafe fn slice(&self, offset: usize, len: usize) -> &[u8] { - unsafe { - let ptr = (self.mem_map_address.Value as *const u8).add(offset); - std::slice::from_raw_parts(ptr, len) - } + Ok(Self { + mem_map, + vars: VarSet::new(var_headers), + buf_len: header.buf_len, + }) } fn next_raw_header(&self) -> Result { // wait for event signal - unsafe { WaitForSingleObject(self.event_handle, TIMEOUT_MS) }; + self.mem_map.wait_for_event_signal(TIMEOUT)?; // read the header - let ptr = self.mem_map_address.Value as *const raw::Header; - let raw_header = unsafe { raw::Header::from_raw_ptr(ptr)? }; + let raw_header = unsafe { self.mem_map.as_raw_header() }?; if raw_header.status != 1 { - return Err(IRacingClientError::Disconnected(None)); + return Err(IRacingClientError::Disconnected); } Ok(raw_header) @@ -115,7 +91,6 @@ impl IRacingClient { pub fn next_sample(&self) -> Result, IRacingClientError> { let raw_header = self.next_raw_header()?; - let header = Header::from_raw(&raw_header)?; let newest_var_buf = raw_header .var_bufs @@ -124,7 +99,10 @@ impl IRacingClient { .process_results(|a| a.max_by_key(|vb| vb.tick_count))? .expect("there are always four var bufs"); - let sample_slice = unsafe { self.slice(newest_var_buf.buf_offset, header.buf_len) }; + let sample_slice = unsafe { + self.mem_map + .as_slice(newest_var_buf.buf_offset, self.buf_len) + }; Ok(Sample::new_as_owned(sample_slice)) } @@ -133,7 +111,6 @@ impl IRacingClient { buf: &'buf mut [u8], ) -> Result, IRacingClientError> { let raw_header = self.next_raw_header()?; - let header = Header::from_raw(&raw_header)?; let newest_var_buf = raw_header .var_bufs @@ -142,7 +119,10 @@ impl IRacingClient { .process_results(|a| a.max_by_key(|vb| vb.tick_count))? .expect("there are always four var bufs"); - let sample_slice = unsafe { self.slice(newest_var_buf.buf_offset, header.buf_len) }; + let sample_slice = unsafe { + self.mem_map + .as_slice(newest_var_buf.buf_offset, self.buf_len) + }; // copy the slice into the buffer buf.clone_from_slice(sample_slice); @@ -157,14 +137,3 @@ impl IRacingClient { self.buf_len } } - -impl Drop for IRacingClient { - fn drop(&mut self) { - unsafe { - // TODO: verify safety. if one of these fails, can we still close the others? - let _ = CloseHandle(self.event_handle); - let _ = UnmapViewOfFile(self.mem_map_address); - let _ = CloseHandle(self.file_mapping_handle); - } - } -} diff --git a/crates/irsdk/src/lib.rs b/crates/irsdk/src/lib.rs index 2bd8e79..96360fe 100644 --- a/crates/irsdk/src/lib.rs +++ b/crates/irsdk/src/lib.rs @@ -1,3 +1,4 @@ mod client; +mod win; pub use client::{IRacingClient, IRacingClientError}; diff --git a/crates/irsdk/src/win.rs b/crates/irsdk/src/win.rs new file mode 100644 index 0000000..561d0d3 --- /dev/null +++ b/crates/irsdk/src/win.rs @@ -0,0 +1,98 @@ +use std::time::Duration; + +use ibt::{RawTelemError, raw}; +use windows::Win32::Foundation::{CloseHandle, GetLastError, HANDLE}; +use windows::Win32::System::Memory::{ + FILE_MAP_READ, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, OpenFileMappingW, UnmapViewOfFile, +}; +use windows::Win32::System::Threading::{ + OpenEventW, SYNCHRONIZATION_SYNCHRONIZE, WaitForSingleObject, +}; +use windows::core::{PCWSTR, w}; + +const MEM_MAP_FILE_NAME: PCWSTR = w!(r"Local\IRSDKMemMapFileName"); +const DATA_VALID_EVENT_NAME: PCWSTR = w!(r"Local\IRSDKDataValidEvent"); +const FILE_NOT_FOUND_CODE: i32 = 0x80070002u32 as i32; + +#[derive(Clone, Debug, thiserror::Error)] +#[error(transparent)] +pub struct WindowsError(#[from] windows::core::Error); + +impl WindowsError { + fn from_last_error() -> Self { + let code = unsafe { GetLastError().to_hresult() }; + let err = windows::core::Error::from_hresult(code); + err.into() + } +} + +#[derive(Clone, Debug, thiserror::Error)] +pub enum SignalError { + #[error("Timeout waiting for signal")] + Timeout, + #[error(transparent)] + Windows(WindowsError), +} + +impl WindowsError { + pub fn is_file_not_found(&self) -> bool { + self.0.code().0 == FILE_NOT_FOUND_CODE + } +} + +#[derive(Debug)] +pub struct TelemetryMemMap { + file_mapping_handle: HANDLE, + mem_map_address: MEMORY_MAPPED_VIEW_ADDRESS, + event_handle: HANDLE, +} + +impl TelemetryMemMap { + pub fn connect() -> Result { + let file_mapping_handle = + unsafe { OpenFileMappingW(FILE_MAP_READ.0, false, MEM_MAP_FILE_NAME)? }; + let mem_map_address = unsafe { MapViewOfFile(file_mapping_handle, FILE_MAP_READ, 0, 0, 0) }; + let event_handle = + unsafe { OpenEventW(SYNCHRONIZATION_SYNCHRONIZE, false, DATA_VALID_EVENT_NAME)? }; + + Ok(Self { + file_mapping_handle, + mem_map_address, + event_handle, + }) + } + + pub fn wait_for_event_signal(&self, timeout: Duration) -> Result<(), SignalError> { + let result = unsafe { WaitForSingleObject(self.event_handle, timeout.as_millis() as u32) }; + // see https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject#return-value + match result.0 { + 0x0 => Ok(()), + 0x102 => Err(SignalError::Timeout), + 0xFFFFFFFF => Err(SignalError::Windows(WindowsError::from_last_error())), + _ => unreachable!(), + } + } + + pub unsafe fn as_raw_header(&self) -> Result { + let ptr = self.mem_map_address.Value as *const raw::Header; + unsafe { raw::Header::from_raw_ptr(ptr) } + } + + pub unsafe fn as_slice(&self, offset: usize, len: usize) -> &[u8] { + unsafe { + let ptr = (self.mem_map_address.Value as *const u8).add(offset); + std::slice::from_raw_parts(ptr, len) + } + } +} + +impl Drop for TelemetryMemMap { + fn drop(&mut self) { + unsafe { + // TODO: verify safety. if one of these fails, can we still close the others? + let _ = CloseHandle(self.event_handle); + let _ = UnmapViewOfFile(self.mem_map_address); + let _ = CloseHandle(self.file_mapping_handle); + } + } +}