Mike Taulty's Blog
Bits and Bytes from Microsoft UK
Silverlight 5 RC–Platform Invocation

Blogs

Mike Taulty's Blog

Elsewhere

Archives

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.


Posted Mon, Sep 5 2011 9:37 AM by mtaulty

Comments

Andries Olivier wrote re: Silverlight 5 RC–Platform Invocation
on Mon, Sep 5 2011 11:36 AM

Hi Mike, good post!

Is it possible to include your own native assembly (C++ for example) to be deployed with your silverlight application and p/invoke, instead of just OS level dll's?

Regards

Andries

mtaulty wrote re: Silverlight 5 RC–Platform Invocation
on Mon, Sep 5 2011 1:30 PM

Hi Andries,

I'm not 100% sure on whether there are clever ways around importing the DLL (e.g. from isostore would be nice) but it does seem that you can use a form such as;

[DllImport("c:\\temp\\MyDll")]

if you've stored the DLL in the filesystem somewhere from your Silverlight XAP file so it does seem that it can work.

Thanks,

Mike.

Paulo Aboim Pinto wrote SL5 Beta com Elevated Trust em aplicações In-Browser
on Mon, Sep 5 2011 3:38 PM

Boas, uma das grandes novidades do Sivlerlight5 é a possibilidade de ter confiança total

Community Blogs wrote What's New in Silverlight 5 RC - Invoking "Run" Dialog from In-Browser Silverlight Application
on Wed, Sep 7 2011 8:23 PM

Silverlight 5 RC (Release Candidate) has been already released. There are some new features already included

David Cuccia wrote re: Silverlight 5 RC–Platform Invocation
on Thu, Sep 8 2011 8:44 AM

I'd be really interested in any follow-up to Andries' question.

Surely, there must be a deployment story...package an .msi as a XAP resource, and have the user download/install (via user-initiated button click)? Could we query the OS for the installation status?

Community Blogs wrote What's New in Silverlight 5 RC - Create Directory from Browser using PInvoke
on Fri, Sep 9 2011 2:05 AM

Yesterday we discussed on Silverlight 5 RC Feature called PInvoke. Hope that was informative for you

Anders Gustafsson wrote re: Silverlight 5 RC–Platform Invocation
on Fri, Sep 16 2011 11:31 AM

Hi @Andries and @David,

I have implemented a workaround for the issue with accessing native third party DLL:s. You should find all the details in this blog post: cureos.blogspot.com/.../pinvoke-bundling-native-dlls-in.html

Any report whether this workaround manages to solve your problems as well is highly appreciated.

Regards,

Anders @ Cureos

Paul wrote re: Silverlight 5 RC–Platform Invocation
on Sun, Oct 9 2011 3:37 PM

For me it works out of browser and in browser on localhost, but on any other host with in browser the calls don't work. Would you have any idea why that would be?

Thanks a lot

Best regards

Paul

Anders Gustafsson wrote re: Silverlight 5 RC–Platform Invocation
on Wed, Oct 12 2011 11:26 AM

Hi @Paul,

I have to admit I have not tried running in-browser on a different host myself, but there should be quite a lot of restrictions for running elevated-trust in-browser applications, making this option primarily viable for intranet deployment. The following blog post describes in more detail what you have to do to enable general in-browser elevated-trust support: www.pitorque.de/.../Silverlight-5-Tidbits-Trusted-applications.aspx

Best regards,

Anders @ Cureos