Binding C APIs with variable-length structs and UTF-16

👋 This page was last updated ~5 years ago. Just so you know.

Okay, I lied.

I'm deciding - right this instant - that using wmic is cheating too. Oh, it was fair game when we were learning about Windows, but we're past that now.

We know there's IPv4 routing tables, and we know network interfaces have indices (yes, they do change when you disable/enable one, so ill-timed configuration changes may make our program blow up).

We also know how to call Win32 APIs directly, and we know that WMI really is more geared toward system administrators than it is toward developers.

There's no way a query language is the canonical way to retrieve that kind of information. Also, we've gone slightly over our crate budget.

Let's fix that right now.

$ cargo rm maplit serde wmi
    Removing maplit from dependencies
    Removing serde from dependencies
    Removing wmi from dependencies

Ahhh. deep sigh. That's better.

(Note that you'll need to remove any code referencing those, especially wmi, thanks to kwas on GitHub for remarking that).

A quick search for "win32 API ipv4 routing tables" returns the GetIpForwardTable function as a second result:

IPHLPAPI_DLL_LINKAGE DWORD GetIpForwardTable(
  PMIB_IPFORWARDTABLE pIpForwardTable,
  PULONG              pdwSize,
  BOOL                bOrder
);

It lives in... well, would you look at that, IPHLPAPI.dll. That sounds familiar!

It looks like it returns the full table (MIB_IPFORWARDTABLE), which itself has rows (MIB_IPFORWARDROW), which in turn contain.. the destination!

typedef struct _MIB_IPFORWARDROW {
  DWORD    dwForwardDest;
  DWORD    dwForwardMask;
  DWORD    dwForwardPolicy;
  DWORD    dwForwardNextHop;
  IF_INDEX dwForwardIfIndex;
  // (other fields cut)
} MIB_IPFORWARDROW, *PMIB_IPFORWARDROW;

I also spy an "IfIndex" in there, and pray that "If" stands for "Network Interface".

Before we truly leave WMI behind, let's make a small C program just to confirm that we can indeed use those Win32 APIs to get the information we want.

We'll keep it real simple. First we'll need to include some headers:

#include <windows.h>
#include <iphlpapi.h>
#include <ipmib.h>

#include <stdio.h>

int main() {
    // C crimes go here

    return 0;
}

Then we'll call GetIpForwardTable once, with a NULL buffer, just so we can query how much size it actually needs.

// in main

DWORD err;
ULONG table_size = 0;

err = GetIpForwardTable(NULL, &table_size, FALSE);
if (err == ERROR_INSUFFICIENT_BUFFER) {
    // good, continue
} else {
    fprintf(stderr, "GetIpForwardTable returned: %x\n", err);
    return 1;
}

printf("table_size = %d\n", table_size);

Let's give it a go.

> cl.exe getiface.c /link iphlpapi.lib
Microsoft (R) C/C++ Optimizing Compiler Version 19.23.28105.4 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

getiface.c
Microsoft (R) Incremental Linker Version 14.23.28105.4
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:getiface.exe
iphlpapi.lib
getiface.obj

> .\getiface.exe
table_size = 2196
Cool bear

Cool bear's hot tip

The above commands are run from "x64 Native Tools Command Prompt for VS 2019", which we installed waaaaaaaaay back in Part 2.

Not shown above: starting PowerShell first, for nicer coloring, completion, and Ctrl+L support (to clear the terminal). Typing cls is only fun the first couple thousand times.

Seems legit. Let's allocate enough space and call it a second time.

// still in main

PMIB_IPFORWARDTABLE table = calloc(table_size, 1);
err = GetIpForwardTable(table, &table_size, FALSE);
if (err == NO_ERROR) {
    // good
} else {
    fprintf(stderr, "GetIpForwardTable (second) returned: %x\n", err);
    return 1;
}

printf("Num entries: %d\n", table->dwNumEntries);

Note that we're using calloc because it zeroes the allocated memory for us.

Is it necessary? I'm not looking it up - you look it up. I'd look it up if it was performance-critical, which it isn't, so I'd rather be on the safe side.

> .\getiface.exe
Num entries: 21

Alright, that seems realistic. 21 routing rules ought to be enough for everyone.

Now, we need to find the routing rule that has 0.0.0.0 as a destination.

As we've found out earlier, IPv4 addresses are typically "just a DWORD". It follows that - no matter what endianness we're into - the address 0.0.0.0 should be just 0.

Let's find it:

// forever in main

int ifaceIndex = -1;
for (int i = 0; i < table->dwNumEntries; i++) {
    if (0 == table->table[i].dwForwardDest) {
        ifaceIndex = table->table[i].dwForwardIfIndex;
        break;
    }
}

if (ifaceIndex == -1) {
    fprintf(stderr, "Default interface not found");
    return 1;
}

printf("Default interface index = %d\n", ifaceIndex);

And the winner is...

> .\getiface.exe
Num entries: 21
Default interface index = 5

Number 5! Just like in rehearsal.

Next up, we need to find the GUID of the network interface.

It just so happens that IPHLPAPI.dll contains a GetInterfaceInfo function, which I think will do nicely.

Its signature is similar:

IPHLPAPI_DLL_LINKAGE DWORD GetInterfaceInfo(
  PIP_INTERFACE_INFO pIfTable,
  PULONG             dwOutBufLen
);

Before using it, let's check what data it actually returns, IP_INTERFACE_INFO:

typedef struct _IP_INTERFACE_INFO {
  LONG                 NumAdapters;
  IP_ADAPTER_INDEX_MAP Adapter[1];
} IP_INTERFACE_INFO, *PIP_INTERFACE_INFO;

Okay, okay, so we'll get all adapters, very good. What's in IP_ADAPTER_INDEX_MAP ?

typedef struct _IP_ADAPTER_INDEX_MAP {
  ULONG Index;
  WCHAR Name[MAX_ADAPTER_NAME];
} IP_ADAPTER_INDEX_MAP, *PIP_ADAPTER_INDEX_MAP;

Ah. Well, at this point we can only hope that Index is, in fact, the same index that GetIpForwardTable was talking about.

I don't see a GUID anywhere, though, do you?

Remarks

The IP_ADAPTER_INDEX_MAP structure is specific to network adapters with IPv4 enabled.

An adapter index may change when the adapter is disabled and then enabled, or under other circumstances, and should not be considered persistent.

On Windows Vista and later, the Name member of the IP_ADAPTER_INDEX_MAP structure may be a Unicode string of the GUID for the network interface (the string begins with the '{' character).

Ah, there it is.

Well this all sounds fine, let's proceed:

// no one can escape the main

err = GetInterfaceInfo(NULL, &table_size);
if (err == ERROR_INSUFFICIENT_BUFFER) {
    // good, continue
} else {
    fprintf(stderr, "GetInterfaceInfo returned: %x\n", err);
    return 1;
}

PIP_INTERFACE_INFO ifaces = calloc(table_size, 1);
err = GetInterfaceInfo(ifaces, &table_size);
if (err == NO_ERROR) {
    // good
} else {
    fprintf(stderr, "GetInterfaceInfo (second) returned: %x\n", err);
    return 1;
}

printf("Listed %d ifaces\n", ifaces->NumAdapters);

for (int i = 0; i < ifaces->NumAdapters; i++) {
    if (ifaces->Adapter[i].Index == ifaceIndex) {
        printf("Found it! Name is: %S", ifaces->Adapter[i].Name);
    }
}
> .\getiface.exe
Num entries: 21
Default interface index = 5
Listed 7 ifaces
Found it! Name is: \DEVICE\TCPIP_{0E89380B-814A-48FC-86C4-5C51B8040CB2}

Success! There is indeed a GUID in there, and it is indeed the same we've gotten through WMI.

Now we just need to quiet sob bind these two functions.

Hello IPHLPAPI my old friend

Luckily, we've made a pretty good LoadLibrary wrapper earlier in the series, so my vote is that we just use that.

In fact, let's just copy the package entirely from sup (our ping clone) into ersatz (our userland re-implementation of various network protocols).

Let's also make a separate module just for getting the default interface GUID.

We'll call it netinfo.

// in `src/main.rs`

mod loadlibrary;
mod netinfo;

fn main() -> Result<(), Error> {
    let interface_name = format!(r#"\Device\NPF_{}"#, netinfo::default_nic_guid()?);
    let lib = open_best_library()?;
    lib.open_interface(&interface_name)?;

    println!("Interface opened!");
    Ok(())
}

// not shown: copying `src/loadlibrary.rs` from `sup` to `ersatz`
// in `src/netinfo.rs`

use crate::error::Error;

pub fn default_nic_guid() -> Result<String, Error> {
    unimplemented!()
}

Now, in Part 6 we made a nice macro for binding Win32 DLLs, but it's stuck in the sup project - and it hardcoded IPHLPAPI.dll, so it's not like it was reusable.

So let's make it reusable!

We'll want the once_cell crate for this:

$ cargo add once_cell
(cut)
// in `src/loadlibrary.rs`, at the bottom

#[macro_export]
macro_rules! bind {
    // new: `library $lib:expr;`
    (library $lib:expr; $(fn $name:ident($($arg:ident: $type:ty),*) -> $ret:ty;)*) => {
        struct Functions {
            $(pub $name: extern "stdcall" fn ($($arg: $type),*) -> $ret),*
        }

        static FUNCTIONS: once_cell::sync::Lazy<Functions> =
            once_cell::sync::Lazy::new(|| {
                let lib = crate::loadlibrary::Library::new($lib).unwrap();
                Functions {
                    $($name: unsafe { lib.get_proc(stringify!($name)).unwrap() }),*
                }
            });

        $(
            #[inline(always)]
            pub fn $name($($arg: $type),*) -> $ret {
                (FUNCTIONS.$name)($($arg),*)
            }
        )*
    };
}

And use it from netinfo. The functions we really want are a tad complicated, so let's just make sure we've copied all the right bits by calling a simple function we know about:

// in `src/netinfo.rs`

#![allow(non_snake_case)]
use crate::error::Error;

pub fn default_nic_guid() -> Result<String, Error> {
    let icmp_file = IcmpCreateFile();
    dbg!(icmp_file);

    unimplemented!()
}

crate::bind! {
    library "IPHLPAPI.dll";

    fn IcmpCreateFile() -> *const std::ffi::c_void;
}

Give it a go:

$ cargo run --quiet
[src\netinfo.rs:6] icmp_file = 0x0000020f44bfc190
thread 'main' panicked at 'not yet implemented', src\netinfo.rs:8:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Fantastic!

Variable-length structs

Now let's try to bind the actual functions. The hard part is not really the functions, but the data structures themselves.

The C declaration for MIB_IPFORWARDTABLE reads as follows:

typedef struct _MIB_IPFORWARDTABLE {
    DWORD dwNumEntries;
    MIB_IPFORWARDROW table[ANY_SIZE];
} MIB_IPFORWARDTABLE, *PMIB_IPFORWARDTABLE;

Can we express that in Rust? Of course we can!

This is perfectly valid:

// we've learned earlier in this series about different
// struct layouts - we definitely want C representation here.

#[repr(C)]
#[derive(Debug)]
pub struct IpForwardTable {
    num_entries: u32,
    entries: [IpForwardRow],
}

#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {}

crate::bind! {
    library "IPHLPAPI.dll";

    fn GetIpForwardTable(table: *mut IpForwardTable, size: *mut u32, order: bool) -> u32;
}

However, if we try to allocate it on the stack:

pub fn default_nic_guid() -> Result<String, Error> {
    let table: IpForwardTable;

    unimplemented!()
}

Then we start running into problems:

$ cargo check --quiet
error[E0277]: the size for values of type `[netinfo::IpForwardRow]` cannot be known at compilation time
 --> src\netinfo.rs:5:9
  |
5 |     let table: IpForwardTable;
  |         ^^^^^ doesn't have a size known at compile-time
  |
  = help: within `netinfo::IpForwardTable`, the trait `std::marker::Sized` is not implemented for `[netinfo::IpForwardRow]`
  = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
  = note: required because it appears within the type `netinfo::IpForwardTable`
  = note: all local variables must have a statically known size
  = help: unsized locals are gated as an unstable feature

We accidentally ended up with a DST - a Dynamically-Sized Type.

At the time of this writing (Rust 1.38.0 is stable), DSTs are kinda awkward to work with. I won't go into details, largely because I don't fully understand them, but the gist is that - as usual, rustc is right. We cannot allocate an IpForwardTable on the stack because we don't know what its size is.

It may have zero entries, or one, or twenty-one, and there is nothing in our code that lets the compiler know how many entries it has. And even if we do specify an array of entries, rustc isn't happy.

My understanding is that, for that particular usecase (variable-length C structs), DSTs are "unfinished". There are pre-RFCs (Requests For Comments) floating around, but nothing quite usable yet. The situation might be better in unstable, but we're going to stick with stable for now.

So, for the time being, we're going to pretend that our entries member is a fixed-size array of size 1:

#[repr(C)]
#[derive(Debug)]
pub struct IpForwardTable {
    num_entries: u32,
    entries: [IpForwardRow; 1],
}

And then this works great:

pub fn default_nic_guid() -> Result<String, Error> {
    let mut table: IpForwardTable;
    let mut size = std::mem::size_of_val(&table) as u32;
    GetIpForwardTable(&mut table, &mut size, false);

    unimplemented!()
}

Wait, no, it doesn't:

$ cargo check --quiet
error[E0381]: borrow of possibly uninitialized variable: `table`
 --> src\netinfo.rs:6:42
  |
6 |     let mut size = std::mem::size_of_val(&table) as u32;
  |                                          ^^^^^^ use of possibly uninitialized `table`

Fair enough. We haven't initialized table. But we don't want to initialize table - I'm pretty sure GetIpForwardTable should initialize table.

Can we use MaybeUninit, as we learned in Part 3 ?

use std::mem::{self, MaybeUninit};

pub fn default_nic_guid() -> Result<String, Error> {
    let mut table: MaybeUninit<IpForwardTable> = MaybeUninit::uninit();
    let mut size = mem::size_of_val(&table) as u32;
    GetIpForwardTable(table.as_mut_ptr(), &mut size, false);

    let table = unsafe { table.assume_init() };
    dbg!(&table);

    std::process::exit(0);
}
$ cargo run --quiet
[src\netinfo.rs:13] &table = IpForwardTable {
    num_entries: 3355314237,
    entries: [
        IpForwardRow,
    ],
}

Uhhh... something's up. I promise I haven't added 3355314216 network interfaces to my computer. Lucky we derived Debug, so it's easy to see something's going wrong!

For starters, we haven't checked GetIpForwardTable's return value.

Win32 error codes are typically just DWORD, so let's make an Error variant for them in our error type:

// in `src/error.rs`

pub enum Error {
    // omitted: Rawsock, IO variants
    Win32(u32),
}

// omitted: Rawsock, IO impls
impl From<u32> for Error {
    fn from(e: u32) -> Self {
        Self::Win32(e)
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            // omitted: Rawsock, IO match arms
            Self::Win32(e) => write!(f, "Win32 error code {} (0x{:x})", e, e),
        }
    }
}

And then use that if we get a non-zero value returned from GetIpForwardTable:

// in `src/netinfo.rs`
// in `default_nic_guid()`

let ret = GetIpForwardTable(table.as_mut_ptr(), &mut size, false);
if ret != 0 {
    return Err(Error::Win32(ret));
}
$ cargo run --quiet
Error: Win32 error code 122 (0x7a)

Ah. Well then. Let's look up that error code (a quick !msdocs system error codes search on DuckDuckGo will do)

ERROR_INSUFFICIENT_BUFFER

122 (0x7A)

The data area passed to a system call is too small.

Source: msdocs

Oh hey, we know that one - we were actually expecting it.

Now we just, uh, make it larger and try again, and uhh

const ERROR_INSUFFICIENT_BUFFER: u32 = 122;

pub fn default_nic_guid() -> Result<String, Error> {
    let mut table: MaybeUninit<IpForwardTable> = MaybeUninit::uninit();
    let mut size = mem::size_of_val(&table) as u32;

    match GetIpForwardTable(table.as_mut_ptr(), &mut size, false) {
        ERROR_INSUFFICIENT_BUFFER => {
            // good, continue
        }
        ret => return Err(Error::Win32(ret)),
    }

    // TODO: how the heck do we make `table` bigger?
    // ❓❓❓
    unimplemented!()
}

The short answer is: we can't. We done goofed. Consequences will never be the same. I'm also pretty sure we've been reported to the cyber police.

How about we flip the problem around. Instead of starting with an IpForwardTable table and trying to make it bigger, how about we start with "raw memory" and try to get an IpForwardTable later, mh?

So, how does one go about allocating raw memory in Rust?

Well, there's std::alloc::alloc:

use std::alloc::{alloc, dealloc, Layout};

unsafe {
    let layout = Layout::new::<u16>();
    let ptr = alloc(layout);

    *(ptr as *mut u16) = 42;
    assert_eq!(*(ptr as *mut u16), 42);

    dealloc(ptr, layout);
}

I'm uhhh not super excited about this. We don't really want to have the responsibility to dealloc by hand (and keep track of the layout!).

How about we use a good old vec instead? That'll be nice.

We'll do it just like in our C version: first call GetIpForwardTable with a null pointer, expecting ERROR_INSUFFICIENT_BUFFER, then allocate the right amount of memory, and call it again, expecting a zero return code.

Vec has as_mut_ptr, which works great for us. We just need to transmute it a bit.

use std::{mem, ptr};

pub fn default_nic_guid() -> Result<String, Error> {
    let mut size = 0;
    match GetIpForwardTable(ptr::null_mut(), &mut size, false) {
        ERROR_INSUFFICIENT_BUFFER => {} // good, continue
        ret => return Err(Error::Win32(ret)),
    }

    let mut v = vec![0u8; size as usize];
    match GetIpForwardTable(unsafe { mem::transmute(v.as_mut_ptr()) }, &mut size, false) {
        0 => {} // good, continue
        ret => return Err(Error::Win32(ret)),
    }

    let table: &IpForwardTable = unsafe { mem::transmute(v.as_ptr()) };
    dbg!(table.num_entries);

    unimplemented!()
}

Does it work?

$ cargo run --quiet
[src\netinfo.rs:20] table.num_entries = 21

Pheww.

That code is, uh, whew, it's something though. Remember how Rust is memory-safe?

Yeah, that's gone now. We forsook that.

In our code, table is just a view into v (the byte vector that actually holds the data), but as far as types and lifetimes (which are mostly types) are concerned, there's nothing linking them, so for example, we can do this:

let table: &IpForwardTable = unsafe { mem::transmute(v.as_ptr()) };
drop(v);
dbg!(table.num_entries);

And kaboom:

$ cargo run --quiet
[src\netinfo.rs:21] table.num_entries = 2248212816

Ah, nothing like the smell of an uninitialized memory read in the morning.

So, this code isn't exactly.. good. If we're not extremely careful, bad things can - nay, will - happen. We'll have to get everyone at the unsafe working group to review our code, and I'm sure they have better things to do.

Plus, we're going to have to do that again, with GetInterfaceInfo, and what are the chances we get it right twice in a row?

A generic "variable length struct" container

Let's use more types.

We want to keep together:

  • our Vec<u8> which contains the actual data
  • the pointer/reference which is a view into that data, with the layout we want

We'd also like, if possible, to have some facility to call Win32 functions twice - once with a null buffer, the second time with a buffer of exactly the right size.

Let's get started:

// in `src/main.rs`

mod vls; // for "variable-length struct"
// in `src/vls.rs`

pub struct VLS<T> {
    v: Vec<u8>,
}

Good start! We definitely want our VLS struct to be generic, since we're going to be using it with IpForwardTable, and later, IpInterfaceInfo.

rustc isn't thrilled though:

$ cargo check --quiet
error[E0392]: parameter `T` is never used
 --> src\vls.rs:1:16
  |
1 | pub struct VLS<T> {
  |                ^ unused parameter
  |
  = help: consider removing `T` or using a marker such as `std::marker::PhantomData`

Alright, alright, who am I to argue:

use std::marker::PhantomData;

pub struct VLS<T> {
    v: Vec<u8>,
    _phantom: PhantomData<T>,
}

This compiles. Good.

Now, we want VLS<T> to behave like T, mostly. Just so we're able to access T's fields, and methods.

So we want to implement std::ops::Deref:

use std::{marker::PhantomData, mem, ops::Deref};

// cut: struct VLS

impl<T> Deref for VLS<T> {
    type Target = T;

    fn deref(&self) -> &T {
        unsafe { mem::transmute(self.v.as_ptr()) }
    }
}

Okay. Is this any better than before? Let's find out.

If we got it right, this should compile:

use crate::vls::VLS;

fn get_vls<T>() -> VLS<T> {
    unimplemented!()
}

pub fn default_nic_guid() -> Result<String, Error> {
    let vls = get_vls::<IpForwardTable>();
    dbg!(vls.num_entries);

    // etc.
}

(And it does).

And this should not compile:

pub fn default_nic_guid() -> Result<String, Error> {
    // allocate...
    let vls = get_vls::<IpForwardTable>();
    // deref to &IpForwardTable...
    let table: &IpForwardTable = &*vls;
    // deallocate
    drop(vls);
    // woops, that's illegal
    dbg!(table);
}

And it doesn't!

$ cargo check --quiet
error[E0505]: cannot move out of `vls` because it is borrowed
  --> src\netinfo.rs:16:10
   |
14 |     let table: &IpForwardTable = &*vls;
   |                                    --- borrow of `vls` occurs here
15 |     // deallocate
16 |     drop(vls);
   |          ^^^ move out of `vls` occurs here
17 |     // woops, that's illegal
18 |     dbg!(table);
   |          ----- borrow later used here

So, when using our VLS type, at least lifetimes are taken care of. VLS owns the data, and when we deref it to whatever C-repr struct, we get a reference whose lifetime must not outlive the VLS instance.

Now for the actual calls. We want to be able to call any Win32 function, but they all work rather the same way:

  • First call with a null pointer, should get ERROR_INSUFFICIENT_BUFFER
  • Second call with a memory block of the right size, should get 0

So, we want our VLS::new signature to look something like this:

use crate::error::Error;

impl<T> VLS<T> {
    pub fn new<F>(f: F) -> Result<Self, Error>
    where
        F: Fn(*mut T, *mut u32) -> u32,
    {
        unimplemented!()
    }
}

So that we can use it like this:

pub fn default_nic_guid() -> Result<String, Error> {
    let table = VLS::new(|ptr, size| GetIpForwardTable(ptr, size, false))?;
    dbg!(table.num_entries);

    unimplemented!()
}

This all compiles, but obviously will panic at runtime, so it's time to implement new for real.

Bear with me for a second:

// in `src/vls.rs`

use std::ptr;

const ERROR_INSUFFICIENT_BUFFER: u32 = 122;

impl<T> VLS<T> {
    pub fn new<F>(f: F) -> Result<Self, Error>
    where
        F: Fn(*mut T, *mut u32) -> u32,
    {
        let mut size = 0;
        match f(ptr::null_mut(), &mut size) {
            ERROR_INSUFFICIENT_BUFFER => {} // good
            ret => return Err(Error::Win32(ret)),
        };

        let mut v = vec![0u8; size as usize];
        match f(unsafe { mem::transmute(v.as_mut_ptr()) }, &mut size) {
            0 => {} // good
            ret => return Err(Error::Win32(ret)),
        };

        Ok(Self {
            v,
            _phantom: PhantomData::default(),
        })
    }
}

Not that bad.

Does it work?

$ cargo run --quiet
[src\netinfo.rs:8] table.num_entries = 21

Yay! Feels pretty good.

We sorta.. forgot one part though. How do we access the actual entries?

We can just implement a method on IpForwardTable:

// in `src/netinfo.rs`

use std::slice;

impl IpForwardTable {
    fn entries(&self) -> &[IpForwardRow] {
        // just like we'd do in C: take the address of the first element.
        // note: this will blow up if `self.num_entries` is incorrect.
        unsafe { slice::from_raw_parts(&self.entries[0], self.num_entries as usize) }
    }
}

pub fn default_nic_guid() -> Result<String, Error> {
    let table = VLS::new(|ptr, size| GetIpForwardTable(ptr, size, false))?;
    dbg!(table.num_entries);
    dbg!(table.entries()); // new!

    unimplemented!()
}
$ cargo run --quiet
[src\netinfo.rs:8] table.num_entries = 21
[src\netinfo.rs:9] table.entries() = [
    IpForwardRow,
    IpForwardRow,
    IpForwardRow,
    IpForwardRow,
    (cut: 17 more entries)
]

Ok, ok, ok. That's ok, we're ok. Everyone's ok.

Those rows look a.. little bit empty though.

#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {}

Oh right.

Well, the C declaration for MIB_IPFORWARDROW looks like this:

typedef struct _MIB_IPFORWARDROW {
    DWORD dwForwardDest;
    DWORD dwForwardMask;
    DWORD dwForwardPolicy;
    DWORD dwForwardNextHop;
    IF_INDEX dwForwardIfIndex;
    union {
        DWORD dwForwardType;              // Old field name uses DWORD type.
        MIB_IPFORWARD_TYPE ForwardType;   // New field name uses enum type.
    };
    union {
        DWORD dwForwardProto;             // Old field name uses DWORD type.
        MIB_IPFORWARD_PROTO ForwardProto; // New field name uses enum type.
    };
    DWORD dwForwardAge;
    DWORD dwForwardNextHopAS;
    DWORD dwForwardMetric1;
    DWORD dwForwardMetric2;
    DWORD dwForwardMetric3;
    DWORD dwForwardMetric4;
    DWORD dwForwardMetric5;
} MIB_IPFORWARDROW, *PMIB_IPFORWARDROW;

So let's, shall we:

#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {
    dest: u32,
    mask: u32,
    policy: u32,
    next_hop: u32,
    if_index: u32,
    _other_fields: [u32; 9],
}

We're mostly interested in dest and if_index, so I kinda.. slapped all the other fields in an u32 array, just so that IpForwardRow has the correct size.

If we didn't do that, the first entry would match, but none of the others would. Remember: we're in full control of how the data in memory gets interpreted, and that is a terrifying amount of power:

That's why _other_fields:

Does it work? Let's take a second to use custom_debug_derive so we can skip _other_fields in our Debug impl:

$ cargo add custom_debug_derive
      Adding custom_debug_derive v0.1.7 to dependencies
use custom_debug_derive::Debug;

#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {
    dest: u32,
    mask: u32,
    policy: u32,
    next_hop: u32,
    if_index: u32,

    #[debug(skip)]
    _other_fields: [u32; 9],
}
$ cargo run --quiet
[src\netinfo.rs:9] table.num_entries = 21
[src\netinfo.rs:10] table.entries() = [
    IpForwardRow {
        dest: 0,
        mask: 0,
        policy: 0,
        next_hop: 4261521600,
        if_index: 5,
    },
    IpForwardRow {
        dest: 127,
        mask: 255,
        policy: 0,
        next_hop: 16777343,
        if_index: 1,
    },
    IpForwardRow {
        dest: 16777343,
        mask: 4294967295,
        policy: 0,
        next_hop: 16777343,
        if_index: 1,
    },
    (cut: 18 more entries)

Alright, alright, that sounds good.

Printing IPv4 addresses as u32 isn't the best though.

We've already made an IPv4 address type earlier in sup, let's add it to ersatz in a new module:

// in `src/main.rs`

mod ipv4;
// in `src/ipv4.rs`

use std::fmt;

// these will come in handy later
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct Addr(pub [u8; 4]);

impl fmt::Display for Addr {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let [a, b, c, d] = self.0;
        write!(f, "{}.{}.{}.{}", a, b, c, d)
    }
}

impl fmt::Debug for Addr {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self)
    }
}

// in `src/netinfo.rs`

use crate::ipv4;

#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {
    dest: ipv4::Addr,
    mask: ipv4::Addr,
    policy: u32,
    next_hop: ipv4::Addr,
    if_index: u32,

    #[debug(skip)]
    _other_fields: [u32; 9],
}

Let's try it out:

$ cargo run --quiet
[src\netinfo.rs:10] table.num_entries = 21
[src\netinfo.rs:11] table.entries() = [
    IpForwardRow {
        dest: 0.0.0.0,
        mask: 0.0.0.0,
        policy: 0,
        next_hop: 192.168.1.254,
        if_index: 5,
    },
    IpForwardRow {
        dest: 127.0.0.0,
        mask: 255.0.0.0,
        policy: 0,
        next_hop: 127.0.0.1,
        if_index: 1,
    },
    IpForwardRow {
        dest: 127.0.0.1,
        mask: 255.255.255.255,
        policy: 0,
        next_hop: 127.0.0.1,
        if_index: 1,
    },
    (cut: 18 more entries)

Fantastic! We can even see the route we need, with dest 0.0.0.0 and if_index 5.

// in `src/netinfo.rs`

pub fn default_nic_guid() -> Result<String, Error> {
    let table = VLS::new(|ptr, size| GetIpForwardTable(ptr, size, false))?;
    let entry = table
        .entries()
        .iter()
        .find(|r| r.dest == ipv4::Addr([0, 0, 0, 0]))
        .expect("should have default interface");
    dbg!(entry);

    unimplemented!()
}
$ cargo run --quiet
[src\netinfo.rs:15] entry = IpForwardRow {
    dest: 0.0.0.0,
    mask: 0.0.0.0,
    policy: 0,
    next_hop: 192.168.1.254,
    if_index: 5,
}

Cool!

Now for the other call.

The declaration for GetInterfaceInfo is easy enough:

crate::bind! {
    library "IPHLPAPI.dll";

    fn GetIpForwardTable(table: *mut IpForwardTable, size: *mut u32, order: bool) -> u32;
    // new!
    fn GetInterfaceInfo(info: *mut IpInterfaceInfo, size: *mut u32) -> u32;
}

So is IP_INTERFACE_INFO:

typedef struct _IP_INTERFACE_INFO {
    LONG    NumAdapters;
    IP_ADAPTER_INDEX_MAP Adapter[1];
} IP_INTERFACE_INFO,*PIP_INTERFACE_INFO;

Interestingly, the size of Adapter here is explicitly set to 1. I guess those C compilers don't do a lot of checking huh.

#[repr(C)]
#[derive(Debug)]
pub struct IpInterfaceInfo {
    num_adapters: u32,
    adapter: [IpAdapterIndexMap; 1],
}

Then we get to IP_ADAPTER_INDEX_MAP:

typedef struct _IP_ADAPTER_INDEX_MAP {
    ULONG   Index;
    WCHAR   Name[MAX_ADAPTER_NAME];
} IP_ADAPTER_INDEX_MAP, *PIP_ADAPTER_INDEX_MAP;

Uh oh, WCHAR. We avoided dealing with UTF-16 back in Part 2, but now there's no way around it.

We know WCHAR is basically an u16, so we can define a newtype like so:

use std::fmt;

pub struct IpAdapterName([u16; 128]);

// Implementing `Display` gives us `.to_string()`
impl fmt::Display for IpAdapterName {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // we assume Windows gave us valid UTF-16
        let s = String::from_utf16_lossy(&self.0[..]);
        // since the name is fixed-size at 128, we want
        // to trim any extra null WCHAR(s) at the end.
        write!(f, "{}", s.trim_end_matches("\0"))
    }
}

// Implementing `Debug` is necessary so we can derive `Debug`
// for `IpAdapterIndexMap`.
impl fmt::Debug for IpAdapterName {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // just use our `Display` implementation with UFCS:
        // https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax
        fmt::Display::fmt(self, f)
    }
}

And shove it in IpAdapterIndexMap

#[repr(C)]
#[derive(Debug)]
pub struct IpAdapterIndexMap {
    index: u32,
    name: IpAdapterName,
}

With all that done, we can call GetInterfaceInfo:

// in `src/netinfo.rs`
// in default_nic_guid()

let ifaces = VLS::new(|ptr, size| GetInterfaceInfo(ptr, size))?;
dbg!(ifaces.num_adapters);
$ cargo run --quiet
(...)
[src\netinfo.rs:19] ifaces.num_adapters = 7

Okay! Now for an adapters method, same technique as IpForwardTable::entries:

impl IpInterfaceInfo {
    fn adapters(&self) -> &[IpAdapterIndexMap] {
        unsafe { slice::from_raw_parts(&self.adapter[0], self.num_adapters as usize) }
    }
}

..and for us to actually use it:

// in default_nic_guid()

let ifaces = VLS::new(|ptr, size| GetInterfaceInfo(ptr, size))?;
let iface = ifaces
    .adapters()
    .iter()
    .find(|r| r.index == entry.if_index)
    .expect("default interface should exist");
dbg!(iface);
$ cargo run --quiet
(...)
[src\netinfo.rs:24] iface = IpAdapterIndexMap {
    index: 5,
    name: \DEVICE\TCPIP_{0E89380B-814A-48FC-86C4-5C51B8040CB2},
}

Success!

We only want to return the GUID, so let's grab it:

// in default_nic_guid()

let name = iface.name.to_string();
let guid_start = name.find("{").expect("interface name should have a guid");
let guid = &name[guid_start..];
Ok(guid.to_string())

Finally, here's our complete netinfo.rs, just shy of 100 lines (counting blank lines):

#![allow(non_snake_case)]
use crate::error::Error;
use crate::ipv4;
use crate::vls::VLS;
use custom_debug_derive::*;
use std::fmt;
use std::slice;

pub fn default_nic_guid() -> Result<String, Error> {
    let table = VLS::new(|ptr, size| GetIpForwardTable(ptr, size, false))?;
    let entry = table
        .entries()
        .iter()
        .find(|r| r.dest == ipv4::Addr([0, 0, 0, 0]))
        .expect("should have default interface");
    dbg!(entry);

    let ifaces = VLS::new(|ptr, size| GetInterfaceInfo(ptr, size))?;
    let iface = ifaces
        .adapters()
        .iter()
        .find(|r| r.index == entry.if_index)
        .expect("default interface should exist");
    dbg!(iface);

    let name = iface.name.to_string();
    let guid_start = name.find("{").expect("interface name should have a guid");
    let guid = &name[guid_start..];
    Ok(guid.to_string())
}

#[repr(C)]
#[derive(Debug)]
pub struct IpForwardTable {
    num_entries: u32,
    entries: [IpForwardRow; 1],
}

impl IpForwardTable {
    fn entries(&self) -> &[IpForwardRow] {
        unsafe { slice::from_raw_parts(&self.entries[0], self.num_entries as usize) }
    }
}

#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {
    dest: ipv4::Addr,
    mask: ipv4::Addr,
    policy: u32,
    next_hop: ipv4::Addr,
    if_index: u32,

    #[debug(skip)]
    _other_fields: [u32; 9],
}

#[repr(C)]
#[derive(Debug)]
pub struct IpInterfaceInfo {
    num_adapters: u32,
    adapter: [IpAdapterIndexMap; 1],
}

impl IpInterfaceInfo {
    fn adapters(&self) -> &[IpAdapterIndexMap] {
        unsafe { slice::from_raw_parts(&self.adapter[0], self.num_adapters as usize) }
    }
}

#[repr(C)]
#[derive(Debug)]
pub struct IpAdapterIndexMap {
    index: u32,
    name: IpAdapterName,
}

pub struct IpAdapterName([u16; 128]);

impl fmt::Display for IpAdapterName {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let s = String::from_utf16_lossy(&self.0[..]);
        write!(f, "{}", s.trim_end_matches("\0"))
    }
}

impl fmt::Debug for IpAdapterName {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(self, f)
    }
}

crate::bind! {
    library "IPHLPAPI.dll";

    fn GetIpForwardTable(table: *mut IpForwardTable, size: *mut u32, order: bool) -> u32;
    fn GetInterfaceInfo(info: *mut IpInterfaceInfo, size: *mut u32) -> u32;
}

Comment on /r/fasterthanlime

(JavaScript is required to see this. Or maybe my stuff broke)

Here's another article just for you:

Understanding Rust futures by going way too deep

So! Rust futures! Easy peasy lemon squeezy. Until it's not. So let's do the easy thing, and then instead of waiting for the hard thing to sneak up on us, we'll go for it intentionally.

Cool bear

Cool bear's hot tip

That's all-around solid life advice.

Choo choo here comes the easy part 🚂💨

We make a new project: