Introduction
There are a lot of parts of the Windows API that are difficult to interface with through the .NET framework due to the liberal use of pointer references. This article provides an example of interfacing to a Windows API call that utilizes a pointer to a large buffer (3 * 256 * 2 bytes large). We also look at how StackAlloc
works and using the unsafe context in our code.
Background
I work in the offshore oil field and marine industry, developing user interfaces to the hardware my company develops. Working offshore, a major concern to those on the bridge of a vessel is the preservation of their night vision. This requires dimming screens down as far as they can go, but many industrial panel mount monitors don't provide brightness controls on the front panel. Using Windows API calls, we can dim the screen down about as far as the hardware dimmer will go.
Using the Code
The Microsoft Windows API provides a call titled SetDeviceGammaRamp
that takes two arguments: an HDC (hardware device context), and a pointer to an array of gamma values to load into the video card. Not all video card architectures support the SetDeviceGammaRamp
, so your mileage with this API call may vary.
Let's take a look at the prototype declaration for the Windows API call:
[DllImport("GDI32.dll")]
private unsafe static extern bool SetDeviceGammaRamp(Int32 hdc, void* ramp);
As you can see, there are a number of parts here. First is the DllImport
; this is contained in the System.Runtime.InteropServices
namespace, and provides the hook into the GDI32.dll where the SetDeviceGammaRamp
function is contained. The function has two arguments, hdc
and ramp
. The first argument, hdc
, is the pointer to the "hardware device context" that we are setting the gamma ramp on. The second argument ramp
is the array of new gamma values, which are stored as a short array with dimensions [3][256].
Let's take a look at the entire class code so we can see everything in context:
[DllImport("gdi32.dll")]
private unsafe static extern bool SetDeviceGammaRamp(Int32 hdc, void* ramp);
private static bool initialized = false;
private static Int32 hdc;
private static void InitializeClass()
{
if (initialized)
return;
hdc = Graphics.FromHwnd(IntPtr.Zero).GetHdc().ToInt32();
initialized = true;
}
public static unsafe bool SetBrightness(short brightness)
{
InitializeClass();
if (brightness > 255)
brightness = 255;
if (brightness < 0)
brightness = 0;
short* gArray = stackalloc short[3 * 256];
short* idx = gArray;
for (int j = 0; j < 3; j++)
{
for (int i = 0; i < 256; i++)
{
int arrayVal = i * (brightness + 128);
if (arrayVal > 65535)
arrayVal = 65535;
*idx = (short)arrayVal;
idx++;
}
}
bool retVal = SetDeviceGammaRamp(hdc, gArray);
return retVal;
}
The first function InitializeClass
is used because we have a static class and we need to store some variables that only need to be found once. Because the class is static, after the InitializeClass
function is called, the variables will be initialized for any caller in the same application context. The line of interest is:
hdc = Graphics.FromHwnd(IntPtr.Zero).GetHdc().ToInt32();
This is where we find our hardware device context. We use the Graphics
class to create a Graphics
object from an Hwnd
of null
(IntPtr.Zero
). This returns a device context that references the entire graphics display. We then use the GetHdc
function to get an IntPtr
to the HDC, and then convert that to an Int32
value that we can pass to the Windows API function.
The function with the meat of this article is SetBrightness
. Let's look at the function declaration:
public static unsafe bool SetBrightness(short brightness)
Here, the function is declared as public
, static
, unsafe
, and returns a type of bool
. The unsafe
keyword means that the entire function is to be considered unsafe; that is, type checking is turned off and pointer use is allowed.
Next, we call InitializeClass
to make sure that the class has been initialized. If the initialization is already done, the function call returns immediately. We then do some checking on the input data (always, always assume that the input data is bad, and clean it as necessary).
short* gArray = stackalloc short[3 * 256];
This line uses stackalloc
to allocate a section of memory on the stack where we can do our work. We can't declare multiple dimension arrays with stackalloc
, so we just set aside as much space as we need for the entire element count. The actual size in bytes of the array is 3 * 256 * sizeof(short)
.
After that, we need to declare an indexing variable that will traverse the allocated array:
short* idx = gArray;
This is initialized to point at the first element in gArray
. We will use this variable to traverse down the array and fill it with the values needed.
for (int j = 0; j < 3; j++)
{
for (int i = 0; i < 256; i++)
{
int arrayVal = i * (brightness + 128);
if (arrayVal > 65535)
arrayVal = 65535;
*idx = (short)arrayVal;
idx++;
}
}
Here, we loop through all the elements in the array and fill it with the desired Gamma values. Idx++
at the end of the loop automatically increases the pointer sizeof(short)
so that we go to the next element. This is the magic of pointer arithmetic.
All that is left is to call the API function with our HDC and Gamma array:
bool retVal = SetDeviceGammaRamp(hdc, gArray)
which is supposed to return true
or false
depending on if the call succeeds, but I've found on my machine that the function call always returns false
, regardless of the function working or not.
And there we have it. It's best to compile this into a separate DLL that you've marked to compile as unsafe (either add the /unsafe
escape to the compiler line, or go to Properties>Build, and check the "Allow Unsafe Code" box).
Points of Interest
Interfacing back-and-forth with .NET code and pointers isn't the easiest thing in the world. Many of us got away from C/C++ because of pointer arithmetic and "DLL Hell", but occasionally, we have to get sucked back in. This just goes to show you that we can write a lot of code to interface to unmanaged code without having to write complicated wrapper functions in separate languages.
Note 1: The API function always returns false
on my machine.