Search code examples
winapirust

Call EnumPrinters from Rust using windows crate


I am trying to call the EnumPrinters function from Rust using the windows crate.

Function signature:

windows::Win32::Graphics::Printing

pub unsafe fn EnumPrintersA<P0>(flags: u32, name: P0, level: u32, pprinterenum: Option<&mut [u8]>, pcbneeded: *mut u32, pcreturned: *mut u32) -> windows_core::Result<()>
where
    P0: windows_core::Param<windows_core::PCSTR>,

Call:

    let flags: u32 = 2; // PRINTER_ENUM_LOCAL
    let name = // ?
    let level: u32 = 4; // PRINTER_INFO_4;
    let pprinterenum = // ?
    let mut pcbneeded = // ?
    let mut pcreturned = // ?

    let result = unsafe { EnumPrintersA(
            flags,
            name,
            level,
            pprinterenum,
            pcb_needed,
            pcreturned,
    ) };

I have the problem that I do not know

  • how the variables have to be declared

  • how the variables have to be passed (for both function calls, especially if null or an empty memory has to be passed)

The function must be called twice with the following parameters:

  1. To determine the size of the required buffer: EnumPrintersA(2, null, 4, null, &pcbneeded, null)

  2. To get the actual data in pprinterenum: EnumPrintersA(2, null, 4, &pprinterenum, &pcbneeded, &pcbreturned)

Of course, a memory the size of pcbneeded must also be allocated for pprinterenum. What is the best way to do this?

Finally, I would like to have a Vec<PRINTER_INFO_4A> that contains the data.


Solution

  • This function is fairly annoying to use, mostly due to the polymorphic data being returned. But here's how to go about it with some tips:

    use windows::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER;
    use windows::Win32::Graphics::Printing::{EnumPrintersA, PRINTER_ENUM_LOCAL, PRINTER_INFO_4A};
    
    let mut needed = 0;
    let mut returned = 0;
    
    let result = unsafe {
        EnumPrintersA(
            PRINTER_ENUM_LOCAL,
            None,
            4,
            None,
            &mut needed,
            &mut returned,
        )
    };
    
    match result {
        Ok(_) => {}
        Err(err) if err.code() == ERROR_INSUFFICIENT_BUFFER.into() => {} // "data area too small" is expected
        Err(err) => {
            // TODO: return error
        }
    }
    

    For the first call, all we're interested in is the needed number. When passing simple out-params like this, it is usually enough to declare it with a default value and passing it as &mut _. The compiler will coerce the mutable reference to a pointer as well as infer the correct type for needed. If you were to declare it with the wrong type, it would error somewhere due to mismatched types. However, it is clear from the EnumPrintersA signature that it is a u32.

    The only tricky one here is the "name" parameter since the function wants a generic Param<PCSTR> type. Fortunately None is sufficient if we want to pass NULL for that.

    The call will return a "too small" error which we anticipate since we provided an empty buffer.


    const INFO_SIZE: usize = std::mem::size_of::<PRINTER_INFO_4A>();
    
    let mut printers = Vec::<PRINTER_INFO_4A>::with_capacity((needed as usize) / INFO_SIZE);
    
    unsafe {
        std::ptr::write_bytes(printers.as_mut_ptr(), 0, printers.capacity());
    }
    
    let result = unsafe {
        EnumPrintersA(
            PRINTER_ENUM_LOCAL,
            None,
            4,
            Some(std::slice::from_raw_parts_mut(
                printers.as_mut_ptr().cast(),
                needed as usize,
            )),
            &mut needed,
            &mut returned,
        )
    };
    
    result.unwrap(); // TODO: handle `result`
    
    assert!(returned as usize <= printers.capacity());
    unsafe { printers.set_len(returned as usize) };
    

    It is a bit annoying because the needed expresses the number of bytes that are to be written, but we ultimately want a Vec<PRINTER_INFO_4A> so it is best to declare it as that upfront with enough capacity by dividing by the ultimate struct size. The std::ptr::write_bytes call to initialize the Vec's allocation is a formality but technically required since we pass it later as a &mut [u8].

    Essentially we pass the memory that we've set aside in the Vec as the buffer to be written to (using std::slice::from_raw_parts_mut to actually pass it). If all goes well (remember to check the result) then we can simply tell the Vec that the data exists via .set_len and you can use printers as you like.