Introducing WinApi: Basics
Prasanna Loganathar
GitHub: WinApi
In the previous article here, I discussed the evolution of programs that use the Windows API with C/C++ and C# snippets, and it ultimately ended out with this C# snippet:
static int Main(string[] args)
{
using (var win = Window.Create(text: "Hello"))
{
win.Show();
return new EventLoop().Run(win);
}
}
Yup. That's fully functional code that works. Just add references to WinApi
, and WinApi.Controls
which are both less than 150kb
combined, and it'll do what its excepted to do. However, before I get into samples that look nifty, let's look at a precise translation of the C/C++ program in the previous article, without the use of the Window
abstraction that WinApi
provides in the helper namespace WinApi.Windows
.
A very raw program that uses the Windows API would look like this:
using System;
using System.Runtime.InteropServices;
using WinApi.Gdi32;
using WinApi.Kernel32;
using WinApi.User32;
namespace Sample.Win32
{
internal class Program
{
static int Main(string[] args)
{
var instanceHandle = Kernel32Methods.GetModuleHandle(IntPtr.Zero);
var wc = new WindowClassEx
{
Size = (uint) Marshal.SizeOf<WindowClassEx>(),
ClassName = "MainWindow",
CursorHandle = User32Helpers.LoadCursor(IntPtr.Zero, SystemCursor.IDC_ARROW),
IconHandle = User32Helpers.LoadIcon(IntPtr.Zero, SystemIcon.IDI_APPLICATION),
Styles = WindowClassStyles.CS_HREDRAW | WindowClassStyles.CS_VREDRAW,
BackgroundBrushHandle = new IntPtr((int) StockObject.WHITE_BRUSH),
WindowProc = WindowProc,
InstanceHandle = instanceHandle
};
var resReg = User32Methods.RegisterClassEx(ref wc);
if (resReg == 0)
{
Console.Error.WriteLine("registration failed");
return -1;
}
var hwnd = User32Methods.CreateWindowEx(WindowExStyles.WS_EX_APPWINDOW,
wc.ClassName, "Hello", WindowStyles.WS_OVERLAPPEDWINDOW,
(int) CreateWindowFlags.CW_USEDEFAULT, (int) CreateWindowFlags.CW_USEDEFAULT,
(int) CreateWindowFlags.CW_USEDEFAULT, (int) CreateWindowFlags.CW_USEDEFAULT,
IntPtr.Zero, IntPtr.Zero, instanceHandle, IntPtr.Zero);
if (hwnd == IntPtr.Zero)
{
Console.Error.WriteLine("window creation failed");
return -1;
}
User32Methods.ShowWindow(hwnd, ShowWindowCommands.SW_SHOWNORMAL);
User32Methods.UpdateWindow(hwnd);
Message msg;
int res;
while ((res = User32Methods.GetMessage(out msg, IntPtr.Zero, 0, 0)) != 0)
{
User32Methods.TranslateMessage(ref msg);
User32Methods.DispatchMessage(ref msg);
}
return res;
}
private static IntPtr WindowProc(IntPtr hwnd, uint umsg,
IntPtr wParam, IntPtr lParam)
{
var msg = (WM) umsg;
switch (msg)
{
case WM.ERASEBKGND:
return new IntPtr(1);
case WM.CLOSE:
{
User32Methods.PostQuitMessage(0);
break;
}
case WM.PAINT:
{
PaintStruct ps;
var hdc = User32Methods.BeginPaint(hwnd, out ps);
User32Methods.FillRect(hdc, ref ps.PaintRect,
Gdi32Helpers.GetStockObject(StockObject.WHITE_BRUSH));
User32Methods.EndPaint(hwnd, ref ps);
break;
}
}
return User32Methods.DefWindowProc(hwnd, umsg, wParam, lParam);
}
}
}
Why would one do this? Because you can. You have no reason to go this really tedious way, however - if you want you always can. The primary function of WinApi
is after-all to provide a clean high-performance interop story.
It does exactly what the previous C/C++ programs did. There's no additional GC pressure, other than loading the CLR before the initialization of Main
itself, the JIT-compiled code would almost precisely match the assembly code of your C++
! Try doing that with Windows Forms. ;)
You can accomplish this in C# yourself, but you have to add all the ugly PInvokes, and method signatures aren't always straight-forward, and even sources like pinvoke.net
aren't always correct and are riddled with subtle errors, all of which WinApi handles.
However, this program above is probably purely academic - not because it has to be, just because there better ways to do this using the WinApi.Windows
namespace.
WinApi structure
Before getting to the WinApi.Windows
namespace, I'd like to briefly touch on how WinApi
is organized. Each library that corresponds to a windows dll
gets its own namespace.
Examples, kernel32.dll
functions all are in the WinApi.Kernel32
namespace, user32.dll
into WinApi.User32
namespace, and so on.
And inside each namespace is a static class which ends with Methods
. Ex: User32Methods
- this class has all the functions of the user32.dll in its most primitive form. What this means is that, if a functions could potentially take multiple forms of input, say different structures, or variables enumerations - this method is the lower common factor, with the least marshalling performance impact.
As an example User32Methods
has the following method defined:
[DllImport(LibraryName, ExactSpelling = true)]
public static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, IntPtr lpPoints, int cPoints);
It takes all inputs in the form of IntPtr
, which allows any kind of marshalling.
At the same time, User32Helpers
has the following implementations:
public static unsafe int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref Point point)
{
fixed (Point* ptr = &point)
return User32Methods.MapWindowPoints(hWndFrom, hWndTo, new IntPtr(ptr), 1);
}
public static unsafe int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref Rectangle rect)
{
fixed (Rectangle* ptr = &rect)
{
var ptPtr = (Point*) ptr;
return User32Methods.MapWindowPoints(hWndFrom, hWndTo, new IntPtr(ptPtr), 2);
}
}
Again, this provides straight forward Marshalling without any performance impact, since it simply pins the struct which already have C-Layouts, and passes in the pointers.
Similar concepts are followed throughout the library in the form of Methods
and Helpers
to provide both raw and safer signatures.
Then, there's also the Experimental
namespace inside each of the library namespaces, that provide all the undocumented functions, and helpers.
WinApi.Windows
Then comes the one namespace that is special - This is not named after any native library. This library provides helpers into Windows
- the literal windows that your applications use. It provides a high-performance, GC-allocation-free message loop, that's resembles ATL/WTL programming.
Class: NativeWindow
This is the class that is the thinnest layer of Window. It simply wraps the original Win32
handle. And it only has a single member - an IntPtr of Handle
, and provides a way to attach itself to any Handle, and allows nice wrappers to be used through out.
var win = WindowFactory.CreateWindowFromHandle(someHwnd);
win.SetText("Hello");
win.SetPosition(100, 200);
win.Show();
win.Close();
Class: WindowFactory
This is the class that acts as a WNDCLASSEX
registration manager for Win32. It registers a class, manages its lifetime, and creates Windows of that particular class.
It also provides all the convenience methods to be able to create classes as NativeWindow
, or as any other derivative of WindowCore
, and provides attachment, and connection implementations. It also has generic methods that are able to project the created class to any C# type that derives from WindowCore
.
Take a look at the source code of the several static methods to see what it does. Naturally, you also provide any CS
styles, background brush, class name and others while creating a new factory - for all practical purposes it is the equivalent of Win32 class registration.
Class WindowCore
Now this is the class where all the magic happens. It provides the actual connection by attaching your handle and connecting your WindowProc
to the class instances. If you look at the internals of ATL
code, this is done using a concept called thunking
and its done in assembly which may seem like dark magic to many. However, WinApi
does this very transparently, and with no performance impact.
It does this with the help of WindowFactory
, and swapping out its procedure during the creation of the window (more precisely during the WM_NCCREATE
message).
Once it provides the connections, the OnMessage
instance method can be used directly from C# to process the messages.
WindowCore
is still a super light weight class that does no message processing. Its stays completely out of the way, except for being able to control your message loop. But the keyword is being able
. It doesn't not by default process any message by itself. Its simply passes it down to it default procedure. This is the lightest class that's fully functional.
Class EventedWindowCore
Directly derived from WindowCore
is the EventedWindowCore
- this class decodes all the window messages, and breaks it down to its components, and passes them down to the relatives class instance's event methods.
For example
public sealed class MainWindow : EventedWindowCore
{
protected override void OnCreate(ref CreateWindowPacket packet)
{
base.OnCreate(ref packet);
}
protected override void OnSize(ref SizePacket packet)
{
var size = packet.Size;
var flags = packet.Flag;
base.OnSize(ref packet);
}
}
Internally it uses the Packetizer
class that simples create a corresponding Packet
struct, and transparently decodes every message into its parameters. The EventedWindowCore
handles most of the common window messages. That's actually all it does. The implementation can be thought of as nothing but one giant switch case that does simply creates a packet and propagates the packet to its appropriate handler method.
For example,
public static unsafe void ProcessMove(ref WindowMessage msg, EventedWindowCore window)
{
fixed (WindowMessage* ptr = &msg)
{
var packet = new MovePacket(ptr);
window.OnMove(ref packet);
}
}
This is the implementation for WM_MOVE
handler. The MovePacket
provides a very nice way to handle WM_MOVE
, since it can both decode or encode the WM_MOVE messages into its parameter. It has the property Point
as one of its properties.
So, similar to ATL/WTL, if you use the WindowCore
you could manually build only the events you want to handle, by using the corresponding Packet
manually - though in really, there's really no reason to do it, since the JIT
will optimize away the EventedWindowCore
to give you similar code in the end.
You can use the EventedWindowCore
, derive all its benefits, but yet maintain performance very similar to writing native code in ATL
with C++. However, for some reason you still want to use WindowCore
, simply create the packet, to be able to decode the messages and build the logic manually, as you like. No more dealing with wparam
and lparam
.
Infact, the way EventedWindowCore
is implemented very similar to this:
public class EventedWindowCore : WindowCore {
protected override OnMessage(ref WindowMessage msg) {
switch (msg.Id) {
...
case WM.MOVE {
Packetizer.ProcessMove(ref msg, this);
break;
}
...
default:
{
this.OnMessageDefault(ref msg);
}
}
}
...
protected internal virtual void OnMove(ref MovePacket packet)
{
// Call the OnMessageDefault here.
}
...
}
And in the above case OnMessageDefault
translates into calling the base window procedure, which would be the DefWindowProc
method in user32.dll
if its an plain window, or the window's default procedure if its an in-built class like STATIC
, EDIT
, etc.
Also, the every single Packet
variant is highly optimized to pass values on without any marshalling and additional copies of data. Even though many of the pointers are very naturally exposed in C# as its counterpart structs, they involve no additional copying. It uses very neat interop techniques to perform reinterpret
casting across the C# managed boundary.
Putting it all together
internal class Program
{
static int Main(string[] args)
{
// Window is just a wrapper over EventedWindowCore,
// that provides more convinience methods, which has
// its own self registered factory.
//
// Window is a part of WinApi.Windows.Controls.
//
// Samples contain code that also directly initiates
// EventedWindowCore without depending on
// WinApi.Windows.Controls simply by creating
// WindowFactory
using (var win = Window.Create<AppWindow>("Hello"))
{
win.Show();
return new EventLoop().Run(win);
}
}
}
public class AppWindow : Window
{
protected override void OnMove(ref MovePacket packet)
{
base.OnMove(ref packet);
}
protected override void OnMessage(ref WindowMessage msg)
{
switch (msg.Id)
{
// Note: OnEraseBkgnd method is already available in
// EventedWindowCore, but directly intercepted here
// just for the sake of overriding the
// message loop.
// Also, note that the message loop is
// short-cicuited here.
case WM.ERASEBKGND:
{
// I can even build the loop only on pay-per-use
// basis, when I need it since all the Packets decoding,
// and encoding are cleanly abstracted away into the Packet
// structs itself.
//
// fixed (var msgPtr = &msg)
// {
// var packet = new EraseBkgndPacket(msg);
// // Do anything you want with the packet.
// }
// return;
msg.Result = new IntPtr(1);
return;
}
}
base.OnMessage(ref msg);
}
}
The Samples included in the WinApi
repository also has several programs that use DirectX, OpenGL, Skia, to paint windows. There are tons of other things that WinApi
does, including one of the simplest ways of using SendInput
with the helpers built right in, DxUtils
that relives all the pain of managing the numerous Direct3D, Direct2D and also manages its versioning with ease, all while maintaining high-performance without any pressure on the GC.
Hopefully, I'll have the time to write about more of those later. But there's already Sample.SimulateInput
as an example of SendInput
and Sample.DirectX
demonstrating DxUtils
in the repo.