Silverlight 5 RC–Platform Invocation

One of the big features of Silverlight 5 for me is platform invocation. If you’ve been around .NET for a while then you’ll know that this has (since day 1) represented the “get out of jail free card” because it allows .NET code to call into pretty much any native code that is packaged as DLL export functions including the Windows API.

In many ways, this is a further extension of the work that was done in Silverlight 4 which allowed Silverlight code to call into existing COM components registered (by ProgId) on the local machine and which had a scripting interface (i.e. IDispatch).

So…Silverlight 4 tried to bridge Silverlight to COM components and Silverlight 5 tries to bridge Silverlight to DLL exports.

It’s worth saying that this functionality is only available to trusted Silverlight applications and only to applications running on Windows (as was the case with the COM functionality in Silverlight 4).

So, how do we go about calling DLL exported functions in Silverlight 5? Let’s build a simple “Process Explorer” kind of application that calls a few Windows APIs.

Step 1 – New Project, Marked as Out-Of-Browser and Trusted

Make a new project and ensure that the project is marked as allowing out-of-browser and requiring elevated trust.

image

Step 2 – Write Some UI

Let’s write some UI that might use a simple DataGrid in order to display some process information;

<UserControl
  x:Class="SilverlightApplication10.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  mc:Ignorable="d"
  d:DesignHeight="300"
  d:DesignWidth="400"
  xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
  xmlns:local="clr-namespace:SilverlightApplication10">
  <UserControl.DataContext>
    <local:ProcessViewModel />
  </UserControl.DataContext>
  <Grid
    x:Name="LayoutRoot"
    Margin="6"
    Background="White">
    <Grid.RowDefinitions>
      <RowDefinition
        Height="Auto" />
      <RowDefinition />
    </Grid.RowDefinitions>
    <TextBlock
      Text="Process Information"
      FontSize="16" />
    <sdk:DataGrid
      Grid.Row="1"
      Margin="2"
      AutoGenerateColumns="true" 
      ItemsSource="{Binding Processes}"/>
  </Grid>
</UserControl>

Step 3 – Write a Simple ViewModel

The crux of that UI is the {Binding Processes} of the DataGrid here and I’ve got a simple view model here behind that;

  public class ProcessViewModel : PropertyChangeNotification
  {
    public ProcessViewModel()
    {
      if (Application.Current.HasElevatedPermissions)
      {
        BuildInitialProcessList();
      }
    }
    void BuildInitialProcessList()
    {
      this.Processes = new ObservableCollection<Process>();
      BuildProcessList();

      DispatcherTimer timer = new DispatcherTimer();
      timer.Interval = TimeSpan.FromMilliseconds(500);
      timer.Tick += (s, e) => BuildProcessList();
      timer.Start();
    }
    void BuildProcessList()
    {
      IEnumerable<Process> newProcesses = Process.EnumerateCurrentList();

      var newProcsOuterJoinedExisting =
        from np in newProcesses
        join op in this.Processes
        on np.Id equals op.Id into joinGroup
        from gp in joinGroup.DefaultIfEmpty()
        select new { NewProcess = np, OldProcess = gp };

      foreach (var item in newProcsOuterJoinedExisting.ToList())
      {
        if (item.OldProcess == null)
        {
          this.Processes.Add(item.NewProcess);
        }
        else
        {
          item.OldProcess.Refresh();
        }
      }

      var remainingListOuterJoinedNewProcs =
        from cp in this.Processes
        join np in newProcesses
        on cp.Id equals np.Id into joinGroup
        from gp in joinGroup.DefaultIfEmpty()
        select new { CurrentProcess = cp, NewProcess = gp };

      foreach (var item in remainingListOuterJoinedNewProcs.ToList())
      {
        if (item.NewProcess == null)
        {
          this.Processes.Remove(item.CurrentProcess);
        }
      }
    }
    public ObservableCollection<Process> Processes
    {
      get
      {
        return (_Processes);
      }
      set
      {
        _Processes = value;
        RaisePropertyChanged("Processes");
      }
    }
    ObservableCollection<Process> _Processes;
  }

We have a collection of Process and we have a helper Process.EnumerateCurrentList and we try and repopulate the list every 500 milliseconds and make sure that we deal with;

  • Adding any new processes that have arrived since our last check to our Processes list
  • Refreshing (line 45 via Refresh) any processes that are still running and were already in our list
  • Getting rid of any processes that were in our Processes list but are not longer running

( at least, that’s what I was trying to do with all that LINQ goo when what I really wanted was a full outer join Winking smile )

Note – this is overly simplistic because process IDs get re-used on Windows and I’m pretending here that they don’t and are unique identifiers so that could cause “confusion” in the real world.

Step 4 – Write Some Interop Code

My Process class is where I hid the P/Invoke code. Here’s the basic class;

  public class Process : PropertyChangeNotification
  {
    [StructLayout(LayoutKind.Sequential)]
    struct PROCESS_MEMORY_COUNTERS
    {
      public UInt32 cb;
      public UInt32 PageFaultCount;
      public UIntPtr PeakWorkingSetSize;
      public UIntPtr WorkingSetSize;
      public UIntPtr QuotaPeakPagedPoolUsage;
      public UIntPtr QuotaPagedPoolUsage;
      public UIntPtr QuotaPeakNonPagedPoolUsage;
      public UIntPtr QuotaNonPagedPoolUsage;
      public UIntPtr PagefileUsage;
      public UIntPtr PeakPagefileUsage;
    };

    public Process(UInt32 processId)
    {
      this.Id = processId;
    }
    public UInt32 Id { get; private set; }

    public UInt64 WorkingSetBytes
    {
      get
      {
        PROCESS_MEMORY_COUNTERS counters;
        IntPtr handle = GetHandle();

        try
        {
          if (!GetProcessMemoryInfo(handle, out counters,
            (UInt32)Marshal.SizeOf(typeof(PROCESS_MEMORY_COUNTERS))))
          {
            throw new Win32Exception("Failed to get memory info",
              Marshal.GetLastWin32Error());
          }
        }
        finally
        {
          CloseHandle(handle);
        }
        return (counters.WorkingSetSize.ToUInt64());
      }
    }

    public void Refresh()
    {
      this.RaisePropertyChanged("WorkingSetBytes");
    }

    public string ImageName
    {
      get
      {
        if (string.IsNullOrEmpty(this.imageName))
        {
          UInt32 capacity = 128;
          StringBuilder builder = new StringBuilder((int)capacity);

          IntPtr handle = GetHandle();

          try
          {
            while (GetProcessImageFileName(handle, builder, capacity) == 0)
            {
              int errorCode = Marshal.GetLastWin32Error();

              if (errorCode == ERROR_INSUFFICIENT_BUFFER)
              {
                capacity *= 2;
                builder = new StringBuilder((int)capacity);
              }
              else
              {
                throw new Win32Exception("Failed to get image name", errorCode);
              }
            }
            this.imageName = Path.GetFileName(builder.ToString());
          }
          finally
          {
            CloseHandle(handle);
          }
        }
        return (this.imageName);
      }
    }
    string imageName;

    IntPtr GetHandle()
    {
      IntPtr handle = OpenProcess(PROCESS_QUERY_INFORMATION, false, this.Id);

      if (handle == IntPtr.Zero)
      {
        throw new Win32Exception("Failed to open process",
          Marshal.GetLastWin32Error());
      }
      return (handle);
    }
    static bool TryOpenProcess(UInt32 id)
    {
      IntPtr ptr = OpenProcess(PROCESS_QUERY_INFORMATION, false, id);

      if (ptr != IntPtr.Zero)
      {
        CloseHandle(ptr);
      }
      return (ptr != IntPtr.Zero);
    }
    public static IEnumerable<Process> EnumerateCurrentList()
    {
      foreach (var processId in EnumerateProcessIds())
      {
        if (TryOpenProcess(processId))
        {
          yield return new Process(processId);
        }
      }
    }
    static IEnumerable<UInt32> EnumerateProcessIds()
    {
      UInt32[] processIds = new UInt32[32];
      bool retry = true;

      while (retry)
      {
        processIds = new UInt32[processIds.Length * 2];

        UInt32 arraySize =
          (UInt32)(Marshal.SizeOf(typeof(UInt32)) * processIds.Length);

        UInt32 bytesCopied = 0;

        retry = EnumProcesses(processIds, arraySize, out bytesCopied);

        if (retry)
        {
          retry = (bytesCopied == arraySize);
        }
        else
        {
          throw new Win32Exception("Failed enumerating processes",
            Marshal.GetLastWin32Error());
        }
      }
      return (processIds);
    }

    [DllImport("psapi", SetLastError = true)]
    static extern bool EnumProcesses(
      [MarshalAs(UnmanagedType.LPArray)] [In] [Out] UInt32[] processIds,
      UInt32 processIdsSizeBytes,
      out UInt32 bytesCopied);

    [DllImport("kernel32", SetLastError = true)]
    static extern IntPtr OpenProcess(UInt32 dwAccess, bool bInheritHandle,
      UInt32 dwProcessId);

    [DllImport("kernel32")]
    static extern bool CloseHandle(IntPtr handle);

    [DllImport("psapi", SetLastError = true)]
    static extern UInt32 GetProcessImageFileName(
      IntPtr processHandle,
      [In] [Out] StringBuilder lpImageFileName,
      UInt32 bufferSizeCharacters);

    [DllImport("psapi", SetLastError = true)]
    static extern bool GetProcessMemoryInfo(
      IntPtr processHandle,
      out PROCESS_MEMORY_COUNTERS counters,
      UInt32 dwSize);

    static readonly UInt32 PROCESS_QUERY_INFORMATION = 0x0400;
    const int ERROR_ACCESS_DENIED = 5;
    const int ERROR_INVALID_PARAMETER = 87;
    const int ERROR_INSUFFICIENT_BUFFER = 122;
  }

and so this is making use of 5 or so Win32 APIs – EnumProcesses, OpenProcess, CloseHandle, GetProcessImageFileName, GetProcessMemoryInfo in order to build up a picture of a Windows process for display.

Note that there’s a bit of a race possible here in that my properties ImageName and WorkingSetBytes might end up stuck with a stale process id and, at the moment, if that happens my code will potentially fail to open the process handle, throw an exception and upset the data-binding so I should really take steps to make that a lot nicer for data-binding.

Here’s my application running out of browser acting as the world’s most basic Task Manager;

image

but it’s a (reasonable) example of the sort of things you can do with platform invocation.

Step 5 – Making it work In-Browser

With Silverlight 5, it’s possible to get this kind of elevated application to work in-browser as I wrote about at some length at the time of the beta.

In the RC, the tooling has been updated to add a new tick-box that doesn’t require you to go to the “out of browser” section at all;

image

Other than that – I followed the exact same process as I did on the blog post that I wrote against the beta and I found myself able to get the application running elevated inside the browser from a URL other than localhost (which always works for testing purposes);

image

Wrapping Up

PInvoke is a big addition to Silverlight and in my first experiments here I found it to work exactly the way I would expect it to work coming from a “full .NET” background – it’s great to see this open up Silverlight apps to calling even more code and scenarios than before.

You can download the code that I wrote for this post here.