use std::ffi::c_void;
use std::fmt;
use std::io;
use std::marker::PhantomData;
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
use std::sync::OnceLock;
use windows_sys::Win32::Foundation::{
DUPLICATE_SAME_ACCESS, DuplicateHandle, FALSE, HANDLE, HMODULE, INVALID_HANDLE_VALUE,
};
use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW};
use windows_sys::Win32::System::Threading::GetCurrentProcess;
use crate::captured::Captured;
type GetCurrentTransactionFn = unsafe extern "system" fn() -> HANDLE;
type SetCurrentTransactionFn = unsafe extern "system" fn(HANDLE) -> u8;
struct Ktm {
get_current: GetCurrentTransactionFn,
set_current: SetCurrentTransactionFn,
}
static KTM: OnceLock<Option<Ktm>> = OnceLock::new();
pub(crate) fn system_proc(dll: &str, name: &[u8]) -> Option<unsafe extern "system" fn() -> isize> {
debug_assert_eq!(
name.last(),
Some(&0),
"a symbol name must be NUL-terminated for GetProcAddress"
);
let wide: Vec<u16> = dll.encode_utf16().chain(std::iter::once(0)).collect();
let module = unsafe { LoadLibraryW(wide.as_ptr()) };
if module.is_null() {
return None;
}
unsafe { GetProcAddress(module as HMODULE, name.as_ptr()) }
}
fn ktm() -> Option<&'static Ktm> {
KTM.get_or_init(|| {
let get = system_proc("ntdll.dll", b"RtlGetCurrentTransaction\0")?;
let set = system_proc("ntdll.dll", b"RtlSetCurrentTransaction\0")?;
unsafe {
Some(Ktm {
get_current: std::mem::transmute::<
unsafe extern "system" fn() -> isize,
GetCurrentTransactionFn,
>(get),
set_current: std::mem::transmute::<
unsafe extern "system" fn() -> isize,
SetCurrentTransactionFn,
>(set),
})
}
})
.as_ref()
}
#[must_use]
pub fn is_supported() -> bool {
ktm().is_some()
}
fn current_raw() -> Option<HANDLE> {
let ktm = ktm()?;
let raw = unsafe { (ktm.get_current)() };
Some(raw)
}
fn is_none_sentinel(raw: HANDLE) -> bool {
raw.is_null() || raw == INVALID_HANDLE_VALUE
}
#[derive(Debug)]
pub struct TransactionContext(OwnedHandle);
impl TransactionContext {
#[must_use]
pub fn as_raw(&self) -> HANDLE {
self.0.as_raw_handle().cast::<c_void>()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TransactionFailure {
Unsupported,
Duplicate,
Install,
}
#[derive(Debug)]
pub struct TransactionError {
failure: TransactionFailure,
source: Option<io::Error>,
}
impl TransactionError {
#[must_use]
pub const fn failure(&self) -> TransactionFailure {
self.failure
}
#[must_use]
pub fn raw_os_error(&self) -> Option<i32> {
self.source.as_ref().and_then(io::Error::raw_os_error)
}
}
impl fmt::Display for TransactionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let what = match self.failure {
TransactionFailure::Unsupported => {
"ktmw32.dll does not offer the thread-transaction entry points"
}
TransactionFailure::Duplicate => "the transaction handle could not be duplicated",
TransactionFailure::Install => "the thread transaction could not be set",
};
match &self.source {
Some(source) => write!(f, "{what}: {source}"),
None => f.write_str(what),
}
}
}
impl std::error::Error for TransactionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|source| source as &(dyn std::error::Error + 'static))
}
}
fn error(failure: TransactionFailure) -> TransactionError {
TransactionError {
failure,
source: Some(io::Error::last_os_error()),
}
}
pub fn capture() -> Result<Captured<TransactionContext>, TransactionError> {
let raw = current_raw().ok_or(TransactionError {
failure: TransactionFailure::Unsupported,
source: None,
})?;
if is_none_sentinel(raw) {
return Ok(Captured::Absent);
}
let mut duplicate: HANDLE = std::ptr::null_mut();
let ok = unsafe {
DuplicateHandle(
GetCurrentProcess(),
raw,
GetCurrentProcess(),
&mut duplicate,
0,
FALSE,
DUPLICATE_SAME_ACCESS,
)
};
if ok == 0 {
return Err(error(TransactionFailure::Duplicate));
}
let owned = unsafe { OwnedHandle::from_raw_handle(duplicate.cast()) };
Ok(Captured::Present(TransactionContext(owned)))
}
fn set_current(raw: HANDLE) -> Result<(), TransactionError> {
let ktm = ktm().ok_or(TransactionError {
failure: TransactionFailure::Unsupported,
source: None,
})?;
let ok = unsafe { (ktm.set_current)(raw) };
if ok == 0 {
return Err(error(TransactionFailure::Install));
}
Ok(())
}
pub fn with_applied<F, T>(
captured: &Captured<TransactionContext>,
operation: F,
) -> Result<T, TransactionError>
where
F: FnOnce() -> T,
{
let guard = install(captured)?;
let outcome = operation();
guard.release().map(|()| outcome)
}
pub fn install<'captured>(
captured: &'captured Captured<TransactionContext>,
) -> Result<TransactionGuard<'captured>, TransactionError> {
let desired = match captured {
Captured::NotCaptured => {
return Ok(TransactionGuard {
previous: None,
released: false,
captured: PhantomData,
});
}
Captured::Absent => std::ptr::null_mut(),
Captured::Present(context) => context.as_raw(),
};
let previous = current_raw().ok_or(TransactionError {
failure: TransactionFailure::Unsupported,
source: None,
})?;
set_current(desired)?;
Ok(TransactionGuard {
previous: Some(previous),
released: false,
captured: PhantomData,
})
}
#[must_use = "dropping the guard restores the transaction but discards any failure to do so"]
#[derive(Debug)]
pub struct TransactionGuard<'captured> {
previous: Option<HANDLE>,
released: bool,
captured: PhantomData<&'captured Captured<TransactionContext>>,
}
impl TransactionGuard<'_> {
pub fn release(mut self) -> Result<(), TransactionError> {
self.released = true;
Self::restore(self.previous)
}
fn restore(previous: Option<HANDLE>) -> Result<(), TransactionError> {
let Some(previous) = previous else {
return Ok(());
};
set_current(if is_none_sentinel(previous) {
std::ptr::null_mut()
} else {
previous
})
}
}
impl Drop for TransactionGuard<'_> {
fn drop(&mut self) {
if !self.released {
let _ = Self::restore(self.previous);
}
}
}
#[cfg(test)]
mod tests;