A Detour: winit & wgpu
Sometimes I read a piece of code that intuitively should not work, yet it is working fine and I just can't. Once in a while, I go and dig deeper to see why.
I take some pride in these moments of inquisitiveness.
Maybe that's because nurturing the curiosity that enables them feels like a rebellion — instead of working on items from to-do lists, thinking about deliverables, milestones, KPIs, ROIs, synergies, and otherwise making the world a better place, I take a scenic detour through the mountains of code.
This post documents one of these detours.
Setting the Scene
winit is a Rust crate (library) that helps with creating windows on different operating systems and platforms.
wgpu is a graphics library for Rust based on the WebGPU API. Applications built with it can run natively on Vulkan, Metal, DirectX 12, OpenGL ES, and in browsers via WebAssembly.
You begin by creating a window using winit. To draw something on it, you first need to create a wgpu surface.
This looks something like this:
let window = winit_event_loop.create_window.unwrap;
let surface = wgpu_instance.create_surface.unwrap;
By looking directly at wgpu's Cargo.toml
(where its dependencies are declared) we can see that wgpu does not have
winit as a dependency. Likewise, winit does not "know" about wgpu either.
How is it possible that a method from one crate takes an instance of a struct from another (seemingly unrelated) crate?
So, let's have a look starting from wgpu's perspective.
Part One: SurfaceTarget
Since we're feeding winit's window to wgpu's create_surface(...)
, this mystery would be solved,
if we could see that winit::Window
is somehow related to wgpu::SurfaceTarget
.
The signature of wgpu's create_surface(...)
method from above is as follows:
Even without knowing much about Rust—and ignoring the 'window
lifetimes— it is hopefully intuitive that
create_surface(...)
takes something that can be converted Into
SurfaceTarget
. So one might expect to find an
implementation of this Into
trait (i.e. interface) for a winit Window
(or
its counterpart, From
),
allowing us to convert the winit window into SurfaceTarget
.
But obviously, there's no point in searching for something like impl From<winit::Window> for wgpu::SurfaceTarget
or
impl Into<wgpu::SurfaceTarget> for winit::Window
in any of these libraries' codebases, because they don't "know"
about each other.
Yet we can easily find this blanket implementation in wgpu's codebase:
It says that we can convert from anything that implements something called WindowHandle
into SurfaceTarget
.
And because Rust implements Into
for us when it has From
, anything that implements WindowHandle
can be converted into surface target. So behind the scenes, there is
impl<T> Into<SurfaceTarget> for T where T: WindowHandle
.
So we now can cut out the middleman—SurfaceTarget
—and our mystery would be solved if we could see that
winit::Window
is somehow related to wgpu::WindowHandle
.
Part Two: WindowHandle
So what kind of thing is wgpu::WindowHandle
? Let's go to its definition:
/// Super trait for window handles as used in [`SurfaceTarget`].
This blanket implementation means that any type can be treated as a WindowHandle as long as it implements the three
required traits. That's a big thing because while WindowHandle
comes from the wgpu
create,
HasWindowHandle
is from another crate called raw_window_handle
.
Now the raw_window_handle
crate seems to be the bridge connecting these two crates (winit
and wgpu
) and the key
to solving this little mystery—since both crates include it as a dependency in their Cargo.toml files.
We must now only verify that winit
's Window
implements HasWindowHandle
and HasDisplayHandle
and
WasmNotSendSync
Part Three: HasWindowHandle
+ HasDisplayHandle
+ WasmNotSendSync
HasWindowHandle
and HasDisplayHandle
are relatively straightforward to find in winit
's codebase'(rwh_06
is an
alias for raw_window_handle version 0.6):
To examine the implementation for WasmNotSendSync
we're going to need to go to go back to wgpu
's codebase' and
(ignoring the conditional compilation attributes for simplicity) there we can find this blanket implementations:
// 1:
// 2:
// 3:
// 4:
// 5:
So, we've got:
WasmNotSendSync
is obviouslyWasmNotSend + WasmNotSync
.- Anything that implements
WasmNotSend
andWasmNotSync
automatically implementsWasmNotSendSync
WasmNotSend
is just a marker trait- Anything can be
WasmNotSend
since there is this blanket implementation. - Anything can be
WasmNotSync
for the same reasons.
Well, it seems like every type implements all these three traits...
Now, one could take yet another detour to understand why these traits are needed in the first place—and potentially, this process is infinite.
Conclusion
I often struggle to make time for detours like this. In a fast-moving world, there are always more urgent tasks — things that feel more “practical” or “effective.” That constant pressure often kills childlike-curiosity these journeys require. But this time, I pushed through — and I’m glad I did.
Thanks for coming along. And if you made it this far, I hope you enjoyed the journey, too.