The Bait and Switch PCL Trick

Some folks on Twitter were debating about how underpowered Portable Libaries are and how shared code is better. While I'm not going to get into this debate (pro-tip, they're wrong :P), not knowing how to "proxy" platform-specific features can lead to a lot of frustration.

Daniel Plaisted has written about one way to expose platform-specific features that works well, but he also taught me a more clever trick once while I was working on Splat.

It took me awhile to understand it fully, but it completely changed my approach to building Portable Libraries and made me realize I was totally Doing It Wrong™. Glenn Block has taken to calling this the "Bait and Switch PCL", whereas Miguel de Icaza has called this the "Advanced PCL" pattern. I'll call it "Bait and Switch" because it's more descriptive :)

If you can get away with it, you don't need this trick

If you can write your library within the constraints of what the Portable Library profiles provides, awesome! You don't need this trick at all. This is definitely a more advanced approach, because you'll need to create platform-specific csproj files for every platform you support. This is a bit of a pain if you're not going to take advantage of it.

Alternate approaches / mistakes

One mistake I made initially while creating Portable Libraries is to think of the Portable version of the library as its own entity - you can see this (mistaken) pattern in the older version of Akavache, where I had a Akavache.Portable NuGet package and an Akavache platform library. This is how most people conceptualize Portable Libraries (past myself included!)

This is the wrong way to do things, don't do it that way!

However, to understand the right way, we need to put together a few disparate facts.

There is no such thing as a portable app

One thing that's important to understand from a philosophical perspective is, the platform that a PCL profile defines doesn't actually exist. No app will ever run under this set of libraries - it's always running under a profile with more features.

NuGet realizes that this is the case, because it will always prefer a platform library to a PCL. This aspect of NuGet is critical to how the Bait and Switch works.

If you take this to its logical conclusion, you realize that, if you provide an Assembly for every platform in the PCL, this means that nobody will ever execute the code in the PCL version, only the platform versions.

We're going to define a PCL that does nothing

Let's implement Splat's BitmapLoader as a Bait and Switch PCL. First, we'll define a class called BitmapLoader (for brevity, I'll only implement Load). First, the return type, which we'll define in both the PCL and the Platform library:

public interface IBitmap  
{
    public float Width { get; }
    public float Height { get; }
}

Now, let's define our bitmap loader in the PCL:

public static class BitmapLoader  
{
    public Task<IBitmap> LoadImage(Stream source)
    {
        return default(Task<IBitmap>);
    }
}

Wait, that's the worst bitmap loader ever. It doesn't even do anything! But, what if the platform-specific version didn't do the same thing? Let's write a UIKit-based one:

public static class BitmapLoader  
{
    public Task<IBitmap> LoadImage(Stream source)
    {
        var data = NSData.FromStream(sourceStream);

        var tcs = new TaskCompletionSource<IBitmap>();
                    UIApplication.SharedApplication.InvokeOnMainThread(() => {
            try {
                tcs.TrySetResult(new CocoaBitmap(UIImage.LoadFromData(data)));
            } catch (Exception ex) {
                tcs.TrySetException(ex);
            }
        });

        return tcs.Task;
    }
}

Okay, so how does that help

Remember that fact I told you about NuGet? If we wrote versions for every platform, then made a NuGet package for this library, it might look something like this:

BitmapLoader.nuspec  
lib/  
    Portable-Net45+NetCore45+MonoTouch+MonoAndroid/
        BitmapLoader.dll     // PCL Version
    Net45/
        BitmapLoader.dll     // WPF Bitmap Loader
    MonoTouch/
        BitmapLoader.dll     // UIKit Bitmap Loader
    MonoAndroid/
        BitmapLoader.dll     // Android Bitmap Loader
    NetCore45/
        BitmapLoader.dll     // WinRT Bitmap Loader

We've covered every platform that the PCL supports, so that means nobody will be using the Portable version (remember what NuGet does from earlier!), except for other portable libraries. Which means, that no real executable will ever run the code in the Portable BitmapLoader.

What even is a method invocation?

But how can we switch out an assembly we referenced for a completely different one?! Doesn't that break? To understand why that doesn't happen, we need to understand what a method invocation really is. Take a look at this monodis output from a random method I picked from Splat:

// method line 40
.method public static hidebysig 
       default valuetype [System.Drawing]System.Drawing.PointF ScaledBy (valuetype [System.Drawing]System.Drawing.PointF This, float32 factor)  cil managed 
{
    .custom instance void class [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::'.ctor'() =  (01 00 00 00 )

    // Method begins at RVA 0x28f5
    // Code size 24 (0x18)
    .maxstack 8
    IL_0000:  ldarga.s 0
    IL_0002:  call instance float32 valuetype [System.Drawing]System.Drawing.PointF::get_X()
    IL_0007:  ldarg.1 
    IL_0008:  mul 
    IL_0009:  ldarga.s 0
    IL_000b:  call instance float32 valuetype [System.Drawing]System.Drawing.PointF::get_Y()
    IL_0010:  ldarg.1 
    IL_0011:  mul 
    IL_0012:  newobj instance void valuetype [System.Drawing]System.Drawing.PointF::'.ctor'(float32, float32)
    IL_0017:  ret 
} // end of method PointMathExtensions::ScaledBy

Don't worry if this makes zero sense. The important part is, at IL_0002 we see a call instruction - while this output is prettified a bit, we can see, that every time we call a method, it's encoding the following information:

FullyQualifiedAssemblyName + Fully Qualified Class Name + Method  

This means, as long as the platform binary matches on assembly name, version, and class structure, we can replace it with the platform binary.

So, that means, your PCL binary and platform binary both have to be called Foo, not Foo.PCL and Foo.Platform or something.

What have we learned

Here's a few takeaways that you can pull from this post that weren't actually found in the post because lazy:

  • If you're referencing assemblies by-hand, always prefer the platform one over the PCL
  • Portable Class Libraries can do way more than they initially appear, if you think about them the Right Way™
  • Platform libraries make for way less #ifdefs and more understandable code because we can just have separate files that implement the same class (i.e. WinRTBitmapLoader.cs, WP8BitmapLoader.cs, etc etc)
  • You have to define all of the types in all of the libraries - don't define types in just the PCL, because the app won't actually reference that PCL. Define all of the types everywhere, even if the PCL version has a dummy implementation.