I have been a proponent of the Rust programming language for a few years and have pitched using it at my jobs. My current work(PDQ.com) has a project that would be perfect for it and I was able to convince our engineering department that we should give it a shot. So far, it's been pretty successful and has been well recieved. The main issues we have run into so far have been around using the windows-rs crate. Which is an autogenerated API that has limited documentation, very little of it Rust specific. I figured I would document some of the trickier things we ended up having to do in case it helps someone else.
One of the things that we needed to do was to subscribe to and report windows events. Should be easy right? Let's go look at the documentation for it. EventLog sounds like a promising start. hmm... Like, 200 items and not a lick of documentation. Let's see if we can find some actual documentation on Microsoft's site. There isn't any direct mapping between the Rust crate and the official documentation that I can discern, but I was able to find this by searching `windows eventlog`. which led me to the corresponding Rust function. which has the following signature
pub unsafe fn EvtSubscribe<'a, Param1: IntoParam<'a, HANDLE>, Param2: IntoParam<'a, PWSTR>, Param3: IntoParam<'a, PWSTR>>( session: isize, signalevent: Param1, channelpath: Param2, query: Param3, bookmark: isize, context: *const c_void, callback: Option<EVT_SUBSCRIBE_CALLBACK>, flags: u32 ) -> isize
🧐 Ok, not terribly helpful. Let's see if we can decipher what some of this is supposed to do.
According to the documentation, it is a session handle to a remote session. If we want to subscribe to local events it should be NULL. Since isize can't be null, lets set it to 0 for now. So far we have:
let session = 0_isize; unsafe { EventLog::EvtSubscribe( session, ... ); }
The next parameter is `signalevent`, which needs to be NULL if we are going to pass a callback, which we are going to do. EZ. Next is the channelpath, which is the event channel we want to subscribe to. We can get a list of event channels by running the following powershell
Get-WinEvent -ListLog *
We are going to use the `Security` channel because it includes the logged in and logged off events and those are easy to generate. The next piece we need is `query`, which needs to be a structured XML query(ew). A list of event codes can be found here. The two codes we're going to use are 4624 and 4634. Our code now looks like
let session = 0_isize; let signal_event = None; let channel_path = "Security"; let query = "EventID=4624 or EventID=4634"; unsafe { EventLog::EvtSubscribe( session, signal_event, channel_path, query ... ); }
The next parameter is `bookmark`, which is only required if the flags parameter that is last contains the `EvtSubscribeStartAfterBookmark` flag, otherwise it should be NULL. Since we aren't going to be using that flag we can set it to 0 the same as the session! Next we have `context` which is used to communicate between the callback we are going to provide and the rest of our program. We're going to skip over this for now and just use `std::ffi::null_mut`. Next we need a callback, which is a function of the following form
pub type EVT_SUBSCRIBE_CALLBACK = unsafe extern "system" fn(action: EVT_SUBSCRIBE_NOTIFY_ACTION, usercontext: *const c_void, event: isize) -> u32;
The first parameter here is an enum that is basically `Result`, it indicates whether there was an error or if there is a notification. That's pretty easy to take care of. We can just check if it's and error and exit early if so. The second param is the context, which is the `null_mut` pointer we made earlier. We're still gonna ignore it for now. The last one is the event handle which we will pass to another `EventLog` api to fetch the actual event, but we'll come back to that. Let's stub out the callback function first. Something important to note is the `#[no_mangle]` attribute. This tells rust to leave the function name as is so that it can be referenced by outside code correctly. We found some odd behavior where the mangling behavior was only breaking things when building the release version for `x86_64-pc-windows-msvc`. When I built the release version on my Linux laptop for the `x86_64-pc-windows-gnu` toolchain, everything worked as expected. But when my coworkers built it on their Windows machine using the msvc target, the program would crash as soon as an event fired.
#[no_mangle] extern "system" fn event_callback( action: EventLog::EVT_SUBSCRIBE_NOTIFY_ACTION, p_context: *const c_void, h_event: isize, ) -> u32 { if action == EventLog::EvtSubscribeActionError { eprintln!("Error in the subscriber: {:?}", action); return 1; } 0 }
Okay, now we are quitting early if there is an error and returning a 0 for success otherwise. Now we need to do something to get the actual event data. We need to use the EvtRender API. The first param is context, which should be NULL if the third param is `EventLog::EvtRenderEventXml`, which is what we want, so we will set it to 0. In our case, the second param is the event handle. The third param will be `EvtRenderEventXml` so that we get the full XML object back. `EvtRenderEventXml` is a newtype tuple wrapper over an i32, so we actually want `EvtRenderEventXml.0`. The fourth param is the buffer size in bytes, which we're going to set at an arbitrary 65000 bytes because that should be far larger than any events. If the buffer is not large enough, the function will fail, but we're going to assume that will be large enough for now. Next we need a buffer which is just a pointer to an array. the second to last param will be the amount of bytes actually used by the event and the last won't be used because we are getting the full XML object, we can set both to 0. Putting all that together we end up with our callback looking like this
#[no_mangle] extern "system" fn event_callback( action: EventLog::EVT_SUBSCRIBE_NOTIFY_ACTION, p_context: *const c_void, event_handle: isize, ) -> u32 { if action == EventLog::EvtSubscribeActionError { eprintln!("Error in the subscriber: {:?}", action); return 1; } let render_context = 0; const BUFFER_SIZE: usize = 65_000; // Windows uses UTF16 for their strings which means that their strings are 16 bytes wide // instead of the normal 8 for rust's UTF8 strings let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE]; let buffer_ptr = buffer.as_mut_ptr() as *mut c_void; let mut used_buffer = 0; let mut property_count = 0; unsafe { EventLog::EvtRender( render_context, event_handle, EventLog::EvtRenderEventXml.0 as u32, BUFFER_SIZE as u32, buffer_ptr, &mut used_buffer, &mut property_count, ); } 0 }
Whew. Okay, we are almost to a point where we can actually look at the events! All we need to do is turn the buffer into a string. Luckily, that part is relatively easy. Rust has some nice std methods that make turning a byte vec into a string easy. Because the buffer we allocated was so big, we need to trim off any trailing 0s to avoid miles of empty space when printing.
#[no_mangle] extern "system" fn event_callback( action: EventLog::EVT_SUBSCRIBE_NOTIFY_ACTION, p_context: *const c_void, event_handle: isize, ) -> u32 { // snip // take a slice of only the used buffer, then turn that into a string let s = String::from_utf16_lossy(&buffer) .trim_matches(char::from(0)) .to_string(); println!("{}", s); 0 }
Now that we have that out of the way, we can finish our subscribe function from above. The last param we need to provide is `flags`, which is an enum that maps to a u32. It describes when we should start subscribing to events. There are a few possible values. We are only interested in future events, so we will use `EventLog::EvtSubscribeToFutureEvents`. Now we can put everything together and try this out.
let session = 0; let signal_event = None; let channel_path = "Security"; let query = "EventID=4624 or EventID=4634"; let bookmark = 0; let context = std::ptr::null_mut(); let flags: u32 = EventLog::EvtSubscribeToFutureEvents.0 as u32; unsafe { EventLog::EvtSubscribe( session, signal_event, channel_path, query bookmark, context, Some(event_callback), ); }
You should see a very large XML string printed to the console. Hooray! This is only mildly useful however. We need to get that information back to our main Rust program so that we can do something with it besides print it to our local terminal. This is getting pretty long so I will be breaking it up into multiple parts. I'll end this part here.