win32 with rust: DirectX 9 starter
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(())
}