다음을 통해 공유


Porting tips: bridging differences in the .Net Framework for Windows Runtime apps

One of the challenges of writing Windows Runtime apps with .NET is that they use a simplified version of the .NET Framework (see .NET for Windows Store apps overview ). For normal coding many of these differences aren’t a major issue: the subset still has all of the functionality needed for Windows Runtime apps, although it may require choosing appropriate methods or using Windows Runtime classes rather than .NET classes.

These differences are more difficult when trying to port code that already targets a different subset of the framework, especially if you want to maintain cross-platform compatibility.

I’ve run into this several times recently helping game developers port Unity 3d games from iOS or Android to Windows and Windows Phone. The same techniques will work to bridge the difference between any two .NET Framework library versions. Here are three methods that can be used to smooth over many of these differences:

  1. Switch between platform-specific behavior with conditional compilation.
  2. Build custom classes mimicking the behavior of unavailable classes.
  3. Attach custom methods to existing classes to replace unavailable methods.

Conditional compilation to choose platform-specific behavior

The most obvious is to abstract the code differences into platform specific classes, methods, or functions. Instead of directly calling file API, call a platform specific method which opens the file and returns the needed data. Depending on the app’s needs this function could return a stream or it could parse and return the data.

 #if NETFX_CORE 
 stream LoadAppData() 
 { 
      // get data with Windows Runtime API 
 } 
 #else 
 stream LoadAppData() 
 { 
     // get data with System.IO 
 }
 #endif

This method works very well if the differences are already well isolated, but if the app scatters the API use throughout the code then it could be a major rewrite.

Replacement classes to add back in missing behavior

In that case a better option may be to replace the missing functionality in place: write a class or method that mimics the original’s interface but calls the new methods.

Again: there are a few options.

The least intrusive is to create new class with the same name in a new namespace. The app can use conditional compilation directives to choose which to use.

Unity does this to mimic several common functions of the System.IO.File class in UnityEngine.Windows.File . Internally UnityEngine.Windows.File calls the asynchronous Windows Runtime classes from Windows.Storage then waits on the Task object to turn the call synchronous. Unity scripts always run on a worker thread not on the UI thread, so they can synchronize File access. Calling Task.Wait on the UI thread will raise an exception)

A game can choose between them by switching its using statements to use UnityEngine.Windows when compiling for the .NET Framework for Windows Store apps and use System.IO on other platforms:  

 #if NETFX_CORE 
 using UnityEngine.Windows; 
 #else 
 using System.IO 
 #endif
  
 …. 
    // The same code will call into the appropriate 
    // version of File for each platform: 
    string data = File.ReadAllBytes(dataPath); 
 ….

There is an example plug-in which defines the same alternative functions in the LegacySystem namespace at https://aka.ms/unityportingsamples. It isn’t needed now that Unity provides the classes in UnityEngine.Windows, but it can be useful sample code if you need to expand for other classes and methods that UnityEngine.Windows doesn’t provide.   

 

Since System.IO.File doesn’t exist at all in the .NET Core framework we could add our own directly to that namespace (make sure you pass valid paths): 

 #if NETFX_CORE 
 namespace System.IO 
 { 
     class File 
     { 
         public static string ReadAllText(string absolutePath) 
         { 
             Task<string> task = global::Windows.Storage.PathIO.ReadTextAsync(absolutePath).AsTask<string>(); 
             task.Wait(); 
             return task.Result; 
         }
  
         public static void WriteAllText(string absolutePath,string contents) 
         { 
             Task task = global::Windows.Storage.PathIO.WriteTextAsync(absolutePath,contents).AsTask(); 
             task.Wait(); 
         } 
  
        // Other File mockups
  
     } 
 #endif

This technique can work the other direction too: Unity’s Mono implementation is old and doesn’t include many newer .NET classes that are available in the .NET 4.5 for Windows Store apps. I was able to add my own version of System.Tuple for use in the Unity player and then use the .NET version #if NETFX_CORE.

Attached properties to add back unavailable methods

If the class already exists but we need methods that aren’t implemented then instead of replacing the whole class we can add back the missing methods as extension methods. For example, I worked with a dev whose code relied on ArrayList.IndexOf which isn’t available in the .NET Framework for Windows Store apps. We were able to add an attached property so he could tag his code onto the built in ArrayList:

 public static class MyExtensions
 {
     public static int IndexOf(this ArrayList alist, object ob)
     {
         for (int i = 0; i < alist.Count; i++)
         {
             if (alist[i] == ob)
                 return i;
         }
         return -1;
     }
 } 

These techniques greatly simplify porting code between similar-but-not-quite-the-same platforms such as Unity3d’s Mono and the .NET for Windows Store apps. You don’t need to rewrite everything to make up for a few easily-shimmable differences.

Next time you’re stuck on a missing API give these a try!

-Rob

Don’t forget to follow the Windows Store Developer Solutions team on Twitter @wsdevsol. Comments are welcome, both below and on twitter.

Comments

  • Anonymous
    January 23, 2015
    IMO the biggest challenge is the fact that some APIs in WinRT are async and have no synchronous counterpart. In your example you mention file access, which is the most obvious example, but you brush aside the issue by saying it's only used in worker threads; it might be true for Unity, but it's not in the general case. The approach I use to write portable code between WinRT and the full framework is to make everything async (well, not everything obviously, but everything that has anything to do with I/O). It's a bit cumbersome, but it works pretty well.

  • Anonymous
    January 23, 2015
    Right. The goal here isn't to allow prohibited functionality such as blocking the UI thread. The goal is to shim non-functional differences: to make it easier to do valid things the same way the legacy code did rather than rewriting code to get the exact same result. For new code I'd always recommend using modern practices rather than trying to shim in old ones, but when one is trying to port existing code it's worth taking a few small steps not to have to redo large ones.