As you may have read, the Xamarin team recently announced the release of a preview of the new Mono interpreter integrated with Xamarin.iOS. The new interpreter has been long anticipated - Miguel hinted at its revival on the Mono blog in late 2017 - and if you lurk the Mono and Xamarin repos you might have noticed that it has been hiding in plain sight for quite some time. In the past, reaping the full benefits of the use of the interpreter has required rolling your own Xamarin.iOS+Mono build with a few changes to keep things like
System.Reflection.Emit (SRE) around, but with this announcement the Xamarin team has put out a special version of Xamarin.iOS (12.7.1) with the right bits baked in, making getting access as easy as downloading and running an installer. Per the announcement blogpost, gaining access to SRE and
Assembly.Load unlocks a whole host of new opportunities in the Xamarin iOS space. Here I want to highlight the opportunities it gives us for performing hot reload of code on the device and where that can be beneficial, as well as point out a few tips and tricks I've come across in my experiments with it.
The case for hot reloading on a device
To be clear -
System.Reflection.Emit have been available to us when working in the simulator for years, the latter enabled by passing the
--enable-repl flag in your
mtouch arguments. What's great about the interpreter is that it lets us use those features on the device, which by proxy means we can execute dynamic code on the device. This is really cool for a whole host of reasons, but (in my opinion) hot reload on the simulator is almost always preferable to reloading on the device, for two main reasons:
- in general, the simulator will perform better than your device, which makes reloading faster
- from a practical perspective, it's easier to interact with the simulator on screen, and the first thing you're likely to do when reloading on a device is mirror it to your screen. If you used the Xamarin Live Player in the past you might have already found this
So if all this magic was already possible on the simulator, and the simulator has a better hot reload workflow, what's so exciting? Well, it turns out that there are some cases where being able to reload on the device is very useful. For example:
- we need to work with device-only features and frameworks like the camera, ARKit and Metal.
- we need to work with frameworks that perform poorly on the simulator (e.g. SpriteKit and SceneKit, which perform abysmally due to the iOS simulator's emulation of OpenGL ES - an Apple problem, not a Xamarin problem)
On a platform involving AOT where build and device deployments take several times longer than simulator deployments, the benefit of hot reload in situations where we absolutely have to use the device is even more pronounced than in normal situations. It's also just pretty epic - to give an example, here's a video of using Frank's Continuous IDE Plugin to hot reload the ARKit image tracking demo I did at my recent Intro to ARKit talk:
Compared to deploying each change and fiddling with node size, colours, scales etc. hot reloading saves a bunch of time here - and this is a demo where I "already knew" what I was doing. In situations where you're learning and experimenting, the productivity boon of 'make a change and see a result' is even greater.
The performance impact of working on the device for frameworks like SpriteKit and SceneKit can also not be overstated - Here's a comparison of a simple fragment shader that attempts to approximate the EarthBound battle background effect - on the left is the source image, in the middle is the shader running on the simulator at 2-5fps and on the right is the shader running on device at a smooth 60fps (gif compression and waiting for it to load for the first time aside).
The difference in performance is worlds apart. The interpreter lets us hot reload things like SpriteKit with good and realistic performance, bringing back that inner loop productivity to development with these frameworks.
(with device-based hot reload via dynamic code execution using the interpreter)
As it turns out, once we have the interpreter going, getting started with device-based dynamic code execution isn't really any different to doing it on the simulator, and there are a few levels of abstraction we can work at:
- Using System.Reflection.Emit APIs directly to define and emit IL
- Using the Mono Evaluator to compile code to IL (uses the SRE APIs under the hood)
- Using Continuous to compile and reload code and UI (uses the Mono evaluator under the hood)
At some point in the future, we should be able to use Roslyn's
CSharpScript api (roughly equivalent in abstraction to the Mono evaluator), but last time I tried - admittedly a few months ago - it wasn't possible.
In all cases, you need to be running the specific preview build of Xamarin.iOS mentioned in the announcement post (or your own build with SRE baked in), and have the
--interpreter flag set in your
mtouch arguments for your device configuration.
Using SRE APIs directly
This is little lower level than I am smart enough to work at, but it does clearly demonstrate the fundamental concept working, and outside of the 'hot reload' space, it opens up the opportunity to use many .NET libraries that rely on IL emitting that have previously been impossible on iOS. For the mere mortals among us, the higher level options below exist. Of course, these options eventually do end up performing something similar to the above.
Using the Mono Evaluator
We can use the Mono Evaluator to compile snippets of C# source code at runtime. Crucially, the evaluator is not limited to just evaluating simple expressions - we can also define and/or instantiate entire new types and use them within the running application. To use the evaluator, first install the
Mono.CSharp NuGet package to your project. A basic evaluation can be set up like below:
In the above screenshot we compile a new class
FuzzyPickles with a
Sum(int,int) method and then invoke it. In this case, we invoke the method within the evaluator, but there's nothing stopping us from doing that outside the evaulator context. For example, if
FuzzyPickles were to instead implement the compile-time known interface
IFuzzyPicklesService, we could evaluate
new FuzzyPickles(), cast the result to
IFuzzyPicklesService and use it in a strongly typed manner from them on. A final way we can access types from the evaluator is by listening for the generated assembly and working on the types themselves. We can use this last method to (for example) register a dynamically created type and then resolve it from a DI container, rather than instantiating it directly.
(Read the note about reliability below for more tips on the evaluator).
Of course, no post from me on anything remotely related to hot reload would be complete without heaping praise on Frank Kruger and his Continuous IDE plugin. Continuous takes dynamic code execution via the evaluator one step further with a dedicated IDE plugin and a presenter, making hot reload as easy as selecting the type you want to start reloading and clicking a button. Even though it hasn't really been touched in a couple of years (I am updating the IDE plugin for to work with VSMac 2019 though), it is still by the far the quickest and easiest method to getting code-based reload for iOS, and I use it most every day that I do iOS development. With a few tricks you can really make it sing - I have a video from a while back showing various 'real world complexities' that it can handle (this was running on the simulator, but the principle is the same):
Of course - the ARKit video from earlier in this post was all being reloaded on the device using Continuous as well.
The readme gives clear instructions on how to use Continuous, it boils down to installing the IDE plugin from the VSMac extension gallery, then installing the NuGet package into your project and calling an initialisation method. Of course, rather than pointing at localhost, be sure to point Continuous at the address of your physical device. Continuous may detect your device with its broadcast support, but I like to put something like this in my
AppDelegate to print the addresses of the device at startup so that I have them handy when starting:
As with above, read the note about reliability and replacing
How reliable is it? What about performance?
Compared to last year, using SRE to hot reload with the interpreter is working really well. I assume that's due to continual improvements from the team, but also possibly due to me eventually working out that I should be replacing
Mono.CSharp from NuGet with one that ships with Xamarin versions. The last published version of
Mono.CSharp is from 2015, so it doesnt support newer C# features and has bugs that affect everyday use and particularly use under the interpreter. Fortunately, up to date versions of the
Mono.CSharp.dll ship with Xamarin.iOS, so it's best to dig into your Xamarin.iOS framework folder and reference the latest version. This gives you access to newer C# features, and since changing to that I haven't been hitting runtime issues when hot reloading. My testing has mostly been focussed on ARKit and SpriteKit and has been pretty much rock solid, I found one hard to run into and easy to workaround issue, but so much works so well already.
In terms of performance, running under the interpreter is inevitably going to incur a performance hit - especially compared to the typical AOT case we're used to on iOS devices. That said, I have been impressed to find it working very well for hot reload purposes (granted, I'm not looking closely for performance impacts). As I understand it, setting the
--interpreter flag with no argument means that everything except
Xamarin.IOS.dll ends up being interpreted -- including
mscorlib. In future crazy real world scenarios where we dynamically load code in our production apps at runtime, we'll likely AOT everything except the dynamic bits, giving us the best of both worlds. In any case, the interpreter is still in preview and performance and reliability is likely to improve over time.
The interpreter opens up a huge range of possibilities, not only related to device-based hot reload. The ability to dynamically execute (well, "interpret") code opens up the possibility of things like hot-patching apps, as well as access to a broader range of the .NET ecosystem's libraries. From where I'm standing, this puts Xamarin.iOS in a very enviable position compared to other frameworks (and even just working with Swift/Objc) - an AOT'd core with native performance characteristics, plus the ability to interpret dynamic code. It's definitely an exciting time to be working in the space and I'm keen to see what the team delivers over the coming months.