Search code examples
windowswinapirustperformancecounter

PdhAddCounterW - no rules expected this token in macro call


I am writing a program to collect Performance Counter. The program is written in Rust and uses the windows crate.

Below is the relevant portion of the code.

let mut query = 0;
PdhOpenQueryW(None, 0, &mut query);
let query_path = CString::new("\\Processor(0)\\% Processor Time");
let mut counter = 0;
PdhAddCounterW(
    query,
    w!(query_path.as_ptr()),
    0,
    &mut counter,
);

The compiler message is:

error: no rules expected the token `query_path`
  --> src\main.rs:12:16
   |
12 |             w!(query_path.as_ptr()),
   |                ^^^^^^^^^^ no rules expected this token in macro call
   |
note: while trying to match meta-variable `$s:literal`
  --> C:\Users\T9SAU2\.cargo\registry\src\github.com-1ecc6299db9ec823\windows-0.48.0\src\core\strings\literals.rs:12:6

I want to explain my analysis and understanding on String in Rust, please let me know where I am going wrong.

When I navigate to C:\Users\T9SAU2\.cargo\registry\src\github.com-1ecc6299db9ec823\windows-0.48.0\src\core\strings\literals.rs:12:6, the relevant code and comment is -

/// A literal UTF-16 wide string with a trailing null terminator.
#[macro_export]
macro_rules! w {
    ($s:literal) => {{
        const INPUT: &[u8] = $s.as_bytes();
        const OUTPUT_LEN: usize = $crate::core::utf16_len(INPUT) + 1;

The input is supposed to be a UTF-16 null-terminated string.

The function signature for PdhAddCounterW() is:

pub unsafe fn PdhAddCounterW<P0>(
    hquery: isize,
    szfullcounterpath: P0,
    dwuserdata: usize,
    phcounter: *mut isize
) -> u32
where
    P0: IntoParam<PCWSTR>,

Link - https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/System/Performance/fn.PdhAddCounterW.html

The argument szfullcounterpath is of type IntoParam\<PCWSTR\>. PCWSTR is defined as:

pub struct PCWSTR(pub *const u16);

A pointer to a constant null-terminated string of 16-bit Unicode characters.

A String in Rust is not null-terminated and is UTF-8. The underlying definition is:

pub struct String {
    vec: Vec<u8>,
}

Basically, a vector of unsigned 8 bits. This will not work.

The argument szfullcounterpath should be a pointer to a null-terminated vector of u16.

str is another type and is valid string slices. This type has an interesting function encode_utf16. Another StackOverflow post confirms the same.

However, when I use the following, I still get the same error:

let mut v: Vec<u16> = query_path.encode_utf16().collect();
v.push(0);

I referred to the blog Rust # 8: Strings. From here, I learned about OsString and CString.

Rust document on OsString states:

On Windows, strings are often arbitrary sequences of non-zero 16-bit values, interpreted as UTF-16 when it is valid to do so.
...
... on Windows, where strings are 16-bit value based as just discussed, strings are also actually stored as a sequence of 8-bit values, encoded in a less-strict variant of UTF-8. This is useful to understand when handling capacity and length values.

The OsString on Windows is UTF-16, but doesn't mention anything about null termination. Also, it is stored as a sequence of u8, hence I am not sure if it can be used without manipulating it to u16 and adding null termination.

CString is a type representing an owned, C-compatible, null-terminated string with no nul bytes in the middle. It is a Box type, hence a smart pointer, but an array of u8. A smart pointer can be converted to a raw pointer, but still this needs to be changed to u16. If I have to use this, I need to iterate over box type and use shifting to convert from u8 to u16.

I believe the easiest option is string slice, yet I receive the same error.

What am I missing?


Solution

  • The w! macro can only be applied to string literals. Your code should thus read:

    // ...
    let mut counter = 0;
    PdhAddCounterW(
        query,
        w!("\\Processor(0)\\% Processor Time"),
        0,
        &mut counter,
    );
    

    If you do need to construct a UTF-16 encoded string at run time and thus cannot use the w! macro, there are several options. Since Rust doesn't provide a string type that uses UTF-16 encoding, they all follow pretty much the same pattern:

    • Construct the string (using Rust's native UTF-8 encoding)
    • Transcode from UTF-8 to UTF-16
    • (Optionally) append a zero terminator

    While you can do that manually, e.g.

    let utf16 = text.encode_utf16().chain(once(0)).collect::<Vec<_>>();
    

    it is far more convenient to use the tools offered by the windows crate instead. All string handling is centered around the HSTRING type, that can be constructed from just about any Rust string type, and also passed into functions that expect a IntoParam<PCWSTR>.

    If query_path is constructed at run time, you can change your code to read

    let mut query = 0;
    PdhOpenQueryW(None, 0, &mut query);
    let query_path = String::from("\\Processor(0)\\% Processor Time");
    let mut counter = 0;
    PdhAddCounterW(
        query,
        &HSTRING::from(&query_path),
        0,
        &mut counter,
    );
    

    All impl From's for HSTRING translate from UTF-8 to UTF-16, and append a zero terminator.