I’ve created a few simple Win32 apps in previous posts to compare C++ and Rust. Even with unsafe Rust via the windows crate, Win32 API programming feels more accessible to me.

This time, I decided to pivot and add COM objects to our Win32 app. My previous examples were based on GDI, hence adding some Direct3D sounds like a good next step. The issue is that it’s been a while since I last used it, so I chose the familiar Direct3D 9 API. Yes, it’s old, but the windows crate supports it in windows::Win32::Graphics::Direct3D9. The module is manageable, and there are plenty of C++ examples. I found [this tutorial] (http://www.directxtutorial.com/Lesson.aspx?lessonid=9-4-1) and decided to replicate it in Rust.

COM in rust#

The article starts with a reminder that Direct3D is a COM object. The windows crate already provides the interface implementation, so we just need to learn a few things about COM in Rust:

  • How to create a COM object.
  • How to release a COM object.
  • How to move or clone a COM object.

Before diving into the code, remember that COM objects are reference counted. Another important thing to note is that all COM interfaces (and thus WinRT classes and interfaces) implement IUnknown.

Creating objects#

Rust offers similar convenience functions for creating a Direct3D9 interface, making code translation straightforward. For example, the C++ code:

#include <d3d9.h>

LPDIRECT3D9 d3d = Direct3DCreate9(D3D_SDK_VERSION);

becomes

use windows::Win32::Graphics::Direct3D9::*;

let d3d = unsafe { Direct3DCreate9(D3D_SDK_VERSION).unwrap() };

Sometimes, object initialization happens via function parameter (out parameter). This pattern is also straightforward to translate.

LPDIRECT3DDEVICE9 d3ddev;
D3DPRESENT_PARAMETERS d3dpp;

ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.Windowed = TRUE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.hDeviceWindow = hWnd;

// CreateDevice initializes d3ddev as it's an out parameter
d3d->CreateDevice(D3DADAPTER_DEFAULT,
                  D3DDEVTYPE_HAL,
                  hWnd,
                  D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                  &d3dpp,
                  &d3ddev);

Could become:

 let mut params = D3DPRESENT_PARAMETERS {
    Windowed: true.into(),
    SwapEffect: D3DSWAPEFFECT_DISCARD,
    hDeviceWindow: window,
    ..Default::default()
};

// although the CreateDevice works with a pointer, working with
// a reference looks nicer (but std::str will also work).
let mut device = None::<IDirect3DDevice9>;

unsafe {
    // CreateDevice initializes device as it's an out parameter
    d3d.CreateDevice(
        D3DADAPTER_DEFAULT,
        D3DDEVTYPE_HAL,
        window,
        D3DCREATE_SOFTWARE_VERTEXPROCESSING as u32,
        &mut params,
        &mut device,
    )?
}

Releasing objects#

The Drop trait is implemented for IUnknown. Since all COM objects wrap it in Rust, there’s no need to manually call Release. The Drop implementation handles this automatically:


impl Drop for IUnknown {
    fn drop(&mut self) {
        unsafe {
            (self.vtable().Release)(core::mem::transmute_copy(self));
        }
    }
}

Moving and copying objects#

Moving objects is simple and requires no special handling. The Clone trait is implemented for IUnknown, automating the reference counting. Its implementation is straightforward:


impl Clone for IUnknown {
    fn clone(&self) -> Self {
        unsafe {
            (self.vtable().AddRef)(core::mem::transmute_copy(self));
        }

        Self(self.0)
    }
}

The code#

Figuring out the COM was the most challenging part. The rest of the code is almost identical to the C++ version, and the simplified version of it is shown in the snippet below.

The only part that is slightly more complex in Rust is the D3DCOLOR handling. C++ provides a couple of macros to simplify that 4-byte value generation. In Rust, we can use the u32 type directly, but we also need to pack alpha, red, green, and blue bytes to the u32 somewhat manually or write a custom helper for it, unless I missed something in the windows crate. Anyway, it’s not a big deal.

use std::ptr;
use windows::{
    core::w,
    Win32::{
        Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM},
        Graphics::Direct3D9::{
            Direct3DCreate9, IDirect3DDevice9, D3DADAPTER_DEFAULT, D3DCLEAR_TARGET,
            D3DCREATE_SOFTWARE_VERTEXPROCESSING, D3DDEVTYPE_HAL, D3DPRESENT_PARAMETERS,
            D3DSWAPEFFECT_DISCARD, D3D_SDK_VERSION,
        },
        System::LibraryLoader::GetModuleHandleW,
        UI::WindowsAndMessaging::{
            CreateWindowExW, DefWindowProcW, DispatchMessageW, LoadCursorW, PeekMessageW,
            PostQuitMessage, RegisterClassW, ShowWindow, TranslateMessage, CW_USEDEFAULT,
            IDC_ARROW, MSG, PM_REMOVE, SW_SHOW, WINDOW_EX_STYLE, WM_DESTROY, WM_QUIT, WNDCLASSW,
            WS_OVERLAPPEDWINDOW,
        },
    },
};

fn main() -> windows::core::Result<()> {
    let instance: HINSTANCE = unsafe { GetModuleHandleW(None)?.into() };

    // ... create and show the window ...

    let mut message = MSG::default();
    {
        // moving the COM object from the function
        let device = create_d3d9_device(window)?;

        unsafe {
            loop {
                while PeekMessageW(&mut message, None, 0, 0, PM_REMOVE).into() {
                    _ = TranslateMessage(&message);
                    DispatchMessageW(&message);
                }

                if message.message == WM_QUIT {
                    break;
                }

                // exit the program on all errors for now
                render_frame(&device)?;
            }
        }

        // Drop trait on COM objects will release them automatically
    }

    std::process::exit(message.wParam.0 as i32);
}

unsafe extern "system" fn proc(
    window: HWND,
    message: u32,
    wparam: WPARAM,
    lparam: LPARAM,
) -> LRESULT {
    match message {
        WM_DESTROY => unsafe {
            PostQuitMessage(0);

            LRESULT(0)
        },
        _ => unsafe { DefWindowProcW(window, message, wparam, lparam) },
    }
}

fn create_d3d9_device(window: HWND) -> windows::core::Result<IDirect3DDevice9> {
    let d3d = unsafe { Direct3DCreate9(D3D_SDK_VERSION).unwrap() };

    let mut params = D3DPRESENT_PARAMETERS {
        Windowed: true.into(),
        SwapEffect: D3DSWAPEFFECT_DISCARD,
        hDeviceWindow: window,
        ..Default::default()
    };

    let mut device = None::<IDirect3DDevice9>;

    unsafe {
        d3d.CreateDevice(
            D3DADAPTER_DEFAULT,
            D3DDEVTYPE_HAL,
            window,
            D3DCREATE_SOFTWARE_VERTEXPROCESSING as u32,
            &mut params,
            &mut device,
        )?
    }

    // move device out of the function
    Ok(device.unwrap())
}

fn render_frame(device: &IDirect3DDevice9) -> windows::core::Result<()> {
    unsafe {
        let color: u32 = 0x00002864;
        device.Clear(0, ptr::null(), D3DCLEAR_TARGET as u32, color, 1.0, 0)?;
        device.BeginScene()?;

        // nothing for now

        device.EndScene()?;
        device.Present(ptr::null(), ptr::null(), HWND::default(), ptr::null())?;
    }
    Ok(())
}