Sunday, 30 September 2012

Handling native API in a managed application

Although Windows 8 and .NET 4.5 have already been released, bringing WinRT with them and promising the end of P/Invoke magic, there's still a lot of time left until programmers can really depend on that. For now, the most widely available way to interact with the underlying operating system from a C# application, when the framework doesn't suffice, remains P/Invoking the Win32 API. In this post I describe my attempt to wrap an interesting part of that API for managed use, pointing out several possible pitfalls.

rusted gears

Lets start with a disclaimer: almost everything you need from your .NET application is doable in clean, managed C# (or VisualBasic or F#). There's usually no need to descend into P/Invoke realms, so please consider again if you really have to break from the safe (and predictable) world of the Framework.

Now take a look at one of the use cases where the Framework does not deliver necessary tooling: I have an application starting several children processes, which may in turn start other processes as well, over which I have no control. But I still need to turn the whole application off, even when one of the grandchild processes breaks in a bad way and stops responding. (If this is really your problem, then take a look at KillUtil.cs from CruiseControl.NET, as this way ultimately what I had to do.)

There is a very nice mechanism for managing child processes in Windows, called Job Objects. I found several partial attempts of wrapping it into a managed API, but nothing really that fitted my purpose. An entry point for grouping processes into jobs is the CreateJobObject function. This is a typical Win32 API call, requiring a structure and a string as parameters. Also, meaning of the parameters might change depending on their values. Not really programmer-friendly. There are a couple of articles on how the native types map into .NET constructs, but it's usually fastest to take a look at PInvoke.net and write your code based on samples there. Keep in mind that it's a wiki and examples will often contain errors.

What kind of errors? For one, they might not consider 32/64 bit compatibility. If it's important to you then be sure to compile your application in both versions - if your P/Invoke signatures aren't proper you'll see some ugly heap corruption exceptions. Other thing often missing from the samples is error checking. Native functions do not throw exceptions, they return status codes and update the global error status, in a couple of different ways. Checking how a particular function communicates failure is probably the most tricky part of wrapping. For that particular method I ended up with the following signature:

[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)]
private static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);

Modifiers static extern are required by P/Invoke mechanism, private is a good practice - calling those methods requires a bit of special handling on the managed side as well. You might also noticed that I omitted the .dll part of the library signature - this doesn't matter on Windows, but Mono will substitute a suitable extension based on the operating system it's running on. For the error reporting to work, it's critical that the status is checked as soon as the method returns. Thus the full call is as follows:

IntPtr result = CreateJobObject(IntPtr.Zero, null);
if (result == IntPtr.Zero)
    throw new Win32Exception();

On failure, this will read the last reported error status and throw a descriptive exception.

Every class holding unmanaged resources should be IDisposable and also include proper cleanup in it's finalizer. Since I'm only storing an IntPtr here I'll skip the finalizer, because I might not want for the job group to be closed in some scenarios. In general that's a bad pattern, it would be better to have a parameter controlling the cleanup instead of "forgetting" the Dispose() call on purpose.

There's quite a lot of tedious set-up code involved in job group control that I won't be discussing in detail (it's at the end of this post if you're interested), but there are a couple of tricks I'd like to point out. First, and pointed out multiple times in P/Invoke documentation (yet still missing from some samples) is the [StructLayout (LayoutKind.Sequential)] attribute, instructing the runtime to lay out your structures in memory exactly as they are in the file. Without that padding might be applied or even the members might get swapped because of memory access optimisation, which would break your native calls in ways difficult to diagnose (especially if the size of the structure would still match).

As I mentioned before, Win32 API calls often vary their parameters meaning based on their values, in some cases expecting differently sized structures. If this happens, information on the size of the structure is also required. Instead of manual counting, you can rely on Marshal.SizeOf (typeof (JobObjectExtendedLimitInformation)) to do this automatically.

Third tip is that native flags are best represented as enum values and OR'ed / XOR'ed as normal .NET enums:

[Flags]
private enum LimitFlags : ushort
{
    JobObjectLimitKillOnJobClose = 0x00002000
}

Wrapping unmanaged API often reveals other problems with it's usage. In this case, first problem was that Windows 7 uses Compatibility Mode for launching Visual Studio, which that wraps it and every program started by it in a job object. Since a job can't (at least not in Windows 7) belong to multiple groups, my new job group assignment would fail and the code would never work inside a debugger. As usual, StackOverflow proved to be helpful in diagnosing and solving this problem.

However, my use case is still not fulfilled: if I add my main process to the job group, it will be terminated as well when I close the group. If I don't, then a child process might spin off children of its own before it is added to the group. In native code, this would be handled by creating the child process as suspended and resuming it only after it has been added to the job object. Unfortunately for me, turns out that Process.Start performs a lot of additional set-up that would be much too time consuming to replicate. Thus I had to go back to the simple KillUtil approach.

I've covered a couple of most common problems with calling native methods from a managed application and presented some useful patterns that make working with them easier. The only part missing is the complete wrapper for the API in question:

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace JobObjectSpike
{
internal sealed class JobObject : IDisposable
{
private readonly IntPtr _jobObjectHandle;
private bool _disposed;
private JobObject(IntPtr jobObjectHandle)
{
_jobObjectHandle = jobObjectHandle;
}
#region IDisposable Members
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
CloseHandleCheckingResult(_jobObjectHandle);
}
#endregion
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)]
private static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
[DllImport("kernel32", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hJob);
[DllImport("kernel32", SetLastError = true)]
private static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoClass jobObjectInfoClass,
[In] ref JobObjectExtendedLimitInformation lpJobObjectInfo,
uint cbJobObjectInfoLength);
[DllImport("kernel32", SetLastError = true)]
private static extern uint ResumeThread(IntPtr hThread);
[DllImport("kernel32", SetLastError = true)]
private static extern bool IsProcessInJob(IntPtr processHandle, IntPtr jobHandle, out bool result);
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool CreateProcess(string lpApplicationName, string lpCommandLine,
IntPtr lpProcessAttributes, IntPtr lpThreadAttributes,
bool bInheritHandles, ProcessCreationFlags dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory, ref StartupInfo lpStartupInfo,
out ProcessInformation lpProcessInformation);
public static JobObject Create()
{
IntPtr result = CreateJobObject(IntPtr.Zero, null);
if (result == IntPtr.Zero)
throw new Win32Exception();
return new JobObject(result);
}
public void AssignCurrentProcess()
{
using (Process process = Process.GetCurrentProcess())
AssignProcess(process);
}
public void AssignProcess(Process process)
{
if (process == null)
throw new ArgumentNullException("process");
AssignProcess(process.Handle);
}
private void CloseHandleCheckingResult(IntPtr handle)
{
bool result = CloseHandle(handle);
if (!result)
throw new Win32Exception();
}
private void AssignProcess(IntPtr handle)
{
if (IsProcessInJobCheckingResult(handle, _jobObjectHandle))
return;
if (IsProcessInJobCheckingResult(handle, IntPtr.Zero))
throw new InvalidOperationException(
"Requested process already belongs to another job group. Check http://stackoverflow.com/a/4232259/3205 for help.");
bool result = AssignProcessToJobObject(_jobObjectHandle, handle);
if (!result)
throw new Win32Exception();
}
private bool IsProcessInJobCheckingResult(IntPtr processHandle, IntPtr jobObjectHandle)
{
bool status;
bool result = IsProcessInJob(processHandle, jobObjectHandle, out status);
if (!result)
throw new Win32Exception();
return status;
}
public void StartProcessInJob(ProcessStartInfo processStartInfo)
{
var startInfo = new StartupInfo();
ProcessInformation processInfo;
// this doesn't seem to set Environment.CurrentDirectory, workaround needed
string workingDirectory = string.IsNullOrEmpty(processStartInfo.WorkingDirectory)
? null
: processStartInfo.WorkingDirectory;
bool result = CreateProcess(processStartInfo.FileName, ' ' + processStartInfo.Arguments, IntPtr.Zero,
IntPtr.Zero,
false, ProcessCreationFlags.CreateSuspended, IntPtr.Zero,
workingDirectory, ref startInfo, out processInfo);
if (!result)
throw new Win32Exception();
AssignProcess(processInfo.hProcess);
uint status = ResumeThread(processInfo.hThread);
if (status != 0 && status != 1)
throw new Win32Exception();
CloseHandleCheckingResult(processInfo.hProcess);
CloseHandleCheckingResult(processInfo.hThread);
}
public void SetKillOnClose()
{
var limit = new JobObjectExtendedLimitInformation
{
BasicLimitInformation =
new JobObjectBasicLimitInformation
{LimitFlags = LimitFlags.JobObjectLimitKillOnJobClose}
};
bool result = SetInformationJobObject(
_jobObjectHandle, JobObjectInfoClass.JobObjectExtendedLimitInformation,
ref limit,
(uint) Marshal.SizeOf(typeof (JobObjectExtendedLimitInformation)));
if (!result)
throw new Win32Exception();
}
#region Nested type: IoCounters
[StructLayout(LayoutKind.Sequential)]
private struct IoCounters
{
public readonly UInt64 ReadOperationCount;
public readonly UInt64 WriteOperationCount;
public readonly UInt64 OtherOperationCount;
public readonly UInt64 ReadTransferCount;
public readonly UInt64 WriteTransferCount;
public readonly UInt64 OtherTransferCount;
}
#endregion
#region Nested type: JobObjectBasicLimitInformation
[StructLayout(LayoutKind.Sequential)]
private struct JobObjectBasicLimitInformation
{
public readonly Int64 PerProcessUserTimeLimit;
public readonly Int64 PerJobUserTimeLimit;
public LimitFlags LimitFlags;
public readonly UIntPtr MinimumWorkingSetSize;
public readonly UIntPtr MaximumWorkingSetSize;
public readonly Int16 ActiveProcessLimit;
public readonly Int64 Affinity;
public readonly Int16 PriorityClass;
public readonly Int16 SchedulingClass;
}
#endregion
#region Nested type: JobObjectExtendedLimitInformation
[StructLayout(LayoutKind.Sequential)]
private struct JobObjectExtendedLimitInformation
{
public JobObjectBasicLimitInformation BasicLimitInformation;
public readonly IoCounters IoInfo;
public readonly UIntPtr ProcessMemoryLimit;
public readonly UIntPtr JobMemoryLimit;
public readonly UIntPtr PeakProcessMemoryUsed;
public readonly UIntPtr PeakJobMemoryUsed;
}
#endregion
#region Nested type: JobObjectInfoClass
private enum JobObjectInfoClass
{
JobObjectExtendedLimitInformation = 9
}
#endregion
#region Nested type: LimitFlags
[Flags]
private enum LimitFlags : ushort
{
JobObjectLimitKillOnJobClose = 0x00002000
}
#endregion
#region Nested type: ProcessCreationFlags
[Flags]
private enum ProcessCreationFlags : uint
{
CreateSuspended = 0x00000004
}
#endregion
#region Nested type: ProcessInformation
[StructLayout(LayoutKind.Sequential)]
public struct ProcessInformation
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
#endregion
#region Nested type: SecurityAttributes
[StructLayout(LayoutKind.Sequential)]
public struct SecurityAttributes
{
public int length;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle;
}
#endregion
#region Nested type: StartupInfo
[StructLayout(LayoutKind.Sequential)]
public struct StartupInfo
{
public uint cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
#endregion
}
}
view raw JobObject.cs hosted with ❤ by GitHub