How To Use C# Events In Unity DLLs
DLLs—like SWCs in Flash—are an extremely handy way to build your code into reusable modules. Unfortunately, Unity has some quirks that can lead to crashes on iOS and other environments that don’t support JIT compilation. The biggest problem that crops up is when you try to use C# events in a DLL. Today’s article investigates why and where the problem occurs and presents a simple solution to work around the problem. Read on to learn how to safely use C# events in Unity DLLs!
Let’s start with a very simple usage of C# events to illustrate the problem:
public class Normal { // Declare a delegate indicating the function signature // of callbacks called by the event public delegate void SomethingHandler(); // Create the event with a default no-op handler public event SomethingHandler OnSomething = () => {}; private void DispatchTheEvent() { // Dispatch the event OnSomething(); } }
When you run a Unity app on iOS or another non-JIT platform and use this event, you’ll get an error like this:
ExecutionEngineException: Attempting to JIT compile method '(wrapper managed-to-native) System.Threading.Interlocked:CompareExchange
To find out why, let’s compile this class to a DLL with Mono 3.12, which is the current version as of this writing. If you use MonoDevelop/Xamarin Studio, Mono is the compiler that’ll be used to compile your DLL. Once it’s compiled, .NET Reflector will decompile the DLL and show the functions for adding and removing callbacks to the event:
public void add_OnSomething(SomethingHandler value) { SomethingHandler handler2; SomethingHandler onSomething = this.OnSomething; do { handler2 = onSomething; onSomething = Interlocked.CompareExchange<SomethingHandler>(ref this.OnSomething, (SomethingHandler) Delegate.Combine(handler2, value), onSomething); } while (onSomething != handler2); } public void remove_OnSomething(SomethingHandler value) { SomethingHandler handler2; SomethingHandler onSomething = this.OnSomething; do { handler2 = onSomething; onSomething = Interlocked.CompareExchange<SomethingHandler>(ref this.OnSomething, (SomethingHandler) Delegate.Remove(handler2, value), onSomething); } while (onSomething != handler2); }
As you can see, the code the compiler generated includes calls to the System.Threading.Interlocked.CompareExchange
function which is attempting to JIT compile per the error message.
At this point it’s a good idea to wonder why this problem doesn’t happen in non-DLL code. To find out, put the same class in your Unity project’s Assets
directory and then decompile the Library/ScriptAssemblies/Assembly-CSharp.dll
that Unity compiles non-DLL project code to. Here’s what you’ll get:
[MethodImpl(MethodImplOptions.Synchronized)] public void add_OnSomething(SomethingHandler value) { this.OnSomething = (SomethingHandler) Delegate.Combine(this.OnSomething, value); } [MethodImpl(MethodImplOptions.Synchronized)] public void remove_OnSomething(SomethingHandler value) { this.OnSomething = (SomethingHandler) Delegate.Remove(this.OnSomething, value); }
Here we see that Unity 4.6 generates entirely different code than Mono 3.12. This code doesn’t include calls to CompareExchange
, so there’s no attempt to JIT and no error. Unfortunately, the code had to be moved out of the DLL to work around this problem. Since we still want to put the code in a DLL, let’s try another compiler: Microsoft Visual Studio 2013.
public void add_OnSomething(SomethingHandler value) { SomethingHandler handler2; SomethingHandler onSomething = this.OnSomething; do { handler2 = onSomething; SomethingHandler handler3 = (SomethingHandler) Delegate.Combine(handler2, value); onSomething = Interlocked.CompareExchange<SomethingHandler>(ref this.OnSomething, handler3, handler2); } while (onSomething != handler2); } public void remove_OnSomething(SomethingHandler value) { SomethingHandler handler2; SomethingHandler onSomething = this.OnSomething; do { handler2 = onSomething; SomethingHandler handler3 = (SomethingHandler) Delegate.Remove(handler2, value); onSomething = Interlocked.CompareExchange<SomethingHandler>(ref this.OnSomething, handler3, handler2); } while (onSomething != handler2); }
This code is similar to the code produced by Mono 3.12, but slightly different. The key part is that it still uses CompareExchange
and will therefore still have the JIT problem.
Given that both compilers are producing event adding and removing code for DLLs that triggers the JIT, let’s try writing our own code to add and remove. Here’s a workaround version:
public class Workaround { // Declare a delegate indicating the function signature // of callbacks called by the event public delegate void SomethingHandler(); // Create an instance of the delegate with a default no-op handler private SomethingHandler somethingInvoker = () => {}; // Create the event with custom add and remove functions public event SomethingHandler OnSomething { // Add the listener to the delegate add { somethingInvoker += value; } // Remove the listener from the delegate remove { somethingInvoker -= value; } } private void DispatchTheEvent() { // Dispatch the delegate, not the event somethingInvoker(); } }
These add
and remove
functions are trivial, but defining them should stop the compiler from generating whatever implementation it wants. Let’s make sure that’s happening by decompiling each DLL. First, here’s the decompilation of the DLL produced by Mono 3.12:
public void add_OnSomething(SomethingHandler value) { this.somethingInvoker = (SomethingHandler) Delegate.Combine(this.somethingInvoker, value); } public void remove_OnSomething(SomethingHandler value) { this.somethingInvoker = (SomethingHandler) Delegate.Remove(this.somethingInvoker, value); }
There are two things to notice here. First and most importantly, neither function uses CompareExchange
anymore. Second, the code is now identical to the add and remove functions generated by the Unity 4.6 compiler. This means that we’ve effectively worked around the JIT issue with Mono 3.12. Next, let’s see if it worked for Visual Studio 2013 by decompiling that version:
public void add_OnSomething(SomethingHandler value) { this.somethingInvoker = (SomethingHandler) Delegate.Combine(this.somethingInvoker, value); } public void remove_OnSomething(SomethingHandler value) { this.somethingInvoker = (SomethingHandler) Delegate.Remove(this.somethingInvoker, value); }
This version is exactly like the Mono 3.12 version and the Unity version, so it should also fix the JIT issue.
There are two downsides to the above approach though. The first is that it requires a fair amount more typing since you have to define your own add
and remove
functions and your own delegate. Here’s a comparison without the comments cluttering it up:
public class Normal { public delegate void SomethingHandler(); public event SomethingHandler OnSomething = () => {}; } public class Workaround { public delegate void SomethingHandler(); private SomethingHandler somethingInvoker = () => {}; public event SomethingHandler OnSomething { add { somethingInvoker += value; } remove { somethingInvoker -= value; } } }
The second downside is that the add
and remove
functions no longer use the more-efficient Interlocked.CompareExchange
function. That’s also the reason that the JIT issue occurred in the first place, but still a downside due to the loss of efficiency. However, this cost is only incurred when adding and removing events. This is normally much less common than actually dispatching events, so the overall impact to the app’s performance should be minimal.
This wraps up the investigation and workaround that allows our Unity apps to use C# events in DLLs. It’s a simple workaround that has minimal performance impact, but still an important one to ensure that the app works on non-JIT platforms like iOS. If you know of any other workaround for this issue, please let me know if the comments!
#1 by Piergiorgio Niero on December 22nd, 2015 ·
First of all thanks for sharing the results of your investigation.
I was wondering whether you went the extra mile and applied this approach to a continuous integration system.
I was thinking about these different options (I didn’t implement any of them yet), but each has its downsides:
1) have the “non JIT” specific code wrapped in preprocessor directives (#define NON_JIT):
pros:
– it’s clear to anyone who opens the class that there is an issue with certain platforms (hence, it has to be tested)
– there’s no code duplication (or better, there’s some, but it’s all in the same place)
cons:
– directives are evaluated at compile time, hence we’d end up with N artifacts per directive (plus permutations!)
– multiple artifacts means multiple build pipelines, hence the build would become really slow
– the approach can easily clutter up the whole codebase, tests included
2) creating a “_nonJIT” implementation of the class (same interface implemented by 2 classes, one with JIT disabled):
pros:
– we’ll end up with 1 artifact
– the approach encourages to use interfaces (works well with an IoC container such as robotlegs sharp, strangeIoC or zenject)
– it’d be very clear examining a bootstrap file (where the mappings between interfaces and implementations are) which classes need the workaround
cons:
– some abstraction layers have to be put in place, leading to a potentially not-so-straightforward code
– the approach can be difficult to implement if the code is not based on a framework, therefore it’s likely we’d end up getting married to a specific framework as the cost of changing it gets higher and higher over time
– many tests would need to run against multiple (two) classes, hence it potentially might slow down the build
– I can envision a large and constantly growing bootstrap file, some typos on the suffix, some new suffixes being created…
Also, my understanding is that the only way of testing that error is to run the code on a non JIT device, so in order to make sure that this nasty issue won’t happen in production the tests need to be run on a device.
Do you have any thought on this?
cheers
–PG
#2 by jackson on December 22nd, 2015 ·
Hi PG, what a thorough comment! :)
I actually have applied this technique to a continuous integration environment. The DLL is built by command line, imported/copied into the Unity project, and then the Unity project can be ran. That Unity project could contain unit tests, integration tests, functional tests, or the app itself. You could also run it on a non-JIT device if you had one plugged into the build machine and the app would run in non-Editor mode (e.g. no NUnit).
As for the options, I think you cover the pros and cons well. The implicit option #0 is the option from the article where you write code that works in JIT and non-JIT environments. It’s more code to write, but at least doesn’t involve preprocessor directives (and therefore multiple artifacts) or multiple implementations (and therefore more duplication). That’s the option I favor, of course. I just wish it wasn’t necessary in the first place.
How about you? Are you using DLLs to split your project into multiple parts? Is it working out well with CI?
#3 by Piergiorgio Niero on January 4th, 2016 ·
Hi Jackson,
Nice to know you used this approach in a CI environment.
I’m still figuring out the best approach, in other languages (AS3, Java\LibGDX) creating libs looks definitely easier.
I’ll keep you posted on how DLLs work for me :)
#4 by Dan on April 9th, 2016 ·
Jackson,
Very cool article! As somebody coming to Unity with a strong C#.NET background, its nice to see other people trying to use good software design practices in the organization and implementation of their scripts. To be honest, I had never seen the add/remove event accessor syntax before. Reading around on MSDN, I found this article that shows the syntax in a typical use case. It says “we recommend that you lock the event before you add or remove a new event handler method.” I would like to avoid this lock block, as it not only clutters the code more, but I could also see it impacting performance if multiple threads were trying to subscribe/unsubscribe to an event. Luckily, I don’t think that multi-threaded scenarios crop up to often in Unity. What do you think? Are the lock blocks inside add/remove still necessary for Unity scripts as MSDN suggests? If so, do you think that they will impact performance?
#5 by jackson on April 9th, 2016 ·
Thanks, Dan! I don’t think adding a
lock
will impact the performance much at all if you’re concerned about multiple threads using the event. Events generally aren’t heavily subscribed to and unsubscribed from so this shouldn’t be a high-traffic chunk of code. It’s also a fast chunk of code so the likelihood that any thread will ever block waiting for the lock is quite slim. Feel free to add locks if you plan to concurrently subscribe and unsubscribe from multiple threads, but that itself is a rare use case that can usually be skipped.#6 by vietmillion on September 16th, 2016 ·
You save my days.
Thanks very much.