Unity’s Burst compiler imposes an interesting subset of C#. The “no managed objects” rule of thumb is not always correct. Today we’ll look at eExceptions, which are managed objects but are partially supported by Burst. What’s allowed and what’s banned? Read on to find out.

Update: A Russian translation of this article is available.

The Burst documentation says:

Burst is working on a subset of .NET that doesn’t allow the usage of any managed objects/reference types in your code (class in C#).

Later it lists disallowed types:

string as this is a managed type

And again, later on:

Any methods related to managed objects (e.g string methods…etc.)

The [BurstDiscard] attribute is said to be useful for when you want to use these managed objects:

When running some code in the full C# (not inside a Burst compiled code), you may want to use some managed objects but you would like to not compile these portion of code when compiling within Burst.

So it seems that exceptions, which are all managed class types, with string messages will not work in Burst-compiled jobs. However, it turns out that there is an exception (pardon the pun) to these rules that make both exceptions and strings sort of work.

Let’s try writing a few test jobs that use exceptions and strings:

using System;
using Unity.Burst;
using Unity.Jobs;
using UnityEngine;
 
class TestScript : MonoBehaviour
{
    [BurstCompile]
    struct ExceptionJob : IJob
    {
        public void Execute()
        {
            throw new ArgumentException("boom");
        }
    }
 
    [BurstCompile]
    struct BeginExceptionJob : IJob
    {
        public int I;
 
        public void Execute()
        {
            throw new ArgumentException("boom" + I);
        }
    }
 
    [BurstCompile]
    struct EndExceptionJob : IJob
    {
        public int I;
 
        public void Execute()
        {
            throw new ArgumentException(I + "boom");
        }
    }
 
    [BurstCompile]
    struct MiddleExceptionJob : IJob
    {
        public int I;
 
        public void Execute()
        {
            throw new ArgumentException(I + "boom" + I);
        }
    }
 
    void Start()
    {
        new ExceptionJob().Schedule().Complete();
        new BeginExceptionJob { I = 10 }.Schedule().Complete();
        new EndExceptionJob { I = 10 }.Schedule().Complete();
        new MiddleExceptionJob { I = 10 }.Schedule().Complete();
    }
}

Now we’ll run it with Unity 2019.1.3f1 and Burst 1.0.0 on macOS and see the results. First of all, this compiles just fine in the editor and the macOS build works. No warnings are produced. When we run the macOS application we see the following logs in Console:

System.ArgumentException: boom
This Exception was thrown from a job compiled with Burst, which has limited exception support. Turn off burst (Jobs -> Enable Burst Compiler) to inspect full exceptions & stacktraces.
 
(Filename:  Line: -1)
 
System.ArgumentException: boom
This Exception was thrown from a job compiled with Burst, which has limited exception support. Turn off burst (Jobs -> Enable Burst Compiler) to inspect full exceptions & stacktraces.
 
(Filename:  Line: -1)
 
System.ArgumentException: boom
This Exception was thrown from a job compiled with Burst, which has limited exception support. Turn off burst (Jobs -> Enable Burst Compiler) to inspect full exceptions & stacktraces.
 
(Filename:  Line: -1)
 
System.ArgumentException: boom
This Exception was thrown from a job compiled with Burst, which has limited exception support. Turn off burst (Jobs -> Enable Burst Compiler) to inspect full exceptions & stacktraces.
 
(Filename:  Line: -1)

Notice that all four jobs produced a System.ArgumentException with the message boom. None of the string concatenation was ever applied so we’re left with just the string literal "boom".

To find out why, let’s look at the Burst Inspector pane in Unity and see the generated assembly for each job type:

; ExceptionJob
movabs  rax, offset .Lburst_abort_Ptr
mov     rax, qword ptr [rax]
movabs  rdi, offset .Lburst_abort.error.id
movabs  rsi, offset .Lburst_abort.error.message
jmp     rax
 
; BeginExceptionJob
movabs  rax, offset .Lburst_abort_Ptr
mov     rax, qword ptr [rax]
movabs  rdi, offset .Lburst_abort.error.id
movabs  rsi, offset .Lburst_abort.error.message
jmp     rax
 
; EndExceptionJob
movabs  rax, offset .Lburst_abort_Ptr
mov     rax, qword ptr [rax]
movabs  rdi, offset .Lburst_abort.error.id
movabs  rsi, offset .Lburst_abort.error.message
jmp     rax
 
; MiddleExceptionJob
movabs  rax, offset .Lburst_abort_Ptr
mov     rax, qword ptr [rax]
movabs  rdi, offset .Lburst_abort.error.id
movabs  rsi, offset .Lburst_abort.error.message
jmp     rax

All four job types were compiled to the exact same assembly. There are no instructions to read the value of I and concatenate it with the "boom" string literal. Instead, we see that .Lburst_abort.error.id and .Lburst_abort.error.message are being output as indicators that an exception occurred and then the program jumps to .Lburst_abort_Ptr. To learn more about these symbols, let’s see those sections of the assembly:

.Lburst_abort.error.id:
        .asciz  "System.ArgumentException"
        .size   .Lburst_abort.error.id, 25
 
        .type   .Lburst_abort.error.message,@object
.Lburst_abort.error.message:
        .asciz  "boom"
        .size   .Lburst_abort.error.message, 5
 
        .type   .Lburst_abort_Ptr,@object
        .local  .Lburst_abort_Ptr
        .comm   .Lburst_abort_Ptr,8,8
        .type   .Lburst_abort.function.string,@object
.Lburst_abort.function.string:
        .asciz  "burst_abort"
        .size   .Lburst_abort.function.string, 12
 
        .section        .debug_str,"MS",@progbits,1

Here we see that .Lburst_abort.error.id has the ASCII (.asciz) string "System.ArgumentException". It has the size (.size) of 25 which is the number of characters in the string plus one for the NUL terminator.

.Lburst_abort.error.message has the ASCII string "boom" with size 5 for the same reasons.

.Lburst_abort_Ptr is in .Lburst_abort.error.message is a memory location used to store the address to jump to when an exception occurs.

So when an exception is thrown in Burst-compiled code, a pointer to the exception type string (e.g. System.ArgumentException) a pointer to the message string (e.g. boom) are written to particular registers and then the program jumps to the .Lburst_abort_Ptr pointer where the exception is presumably handled by reading from those registers.

At no point do we see any memory allocation, only the storing of pointers to string literals stored in the program’s data section. This is possibly the reason that string concatenation is not done, since it would require additional work to allocate, potentially dynamically grow, and finally deallocation after the exception is handled.

So feel free to throw exceptions in Burst-compiled jobs, so long as the message is a string literal or concatenated variables aren’t needed. But keep in mind that catch and finally aren’t allowed in Burst-compiled jobs, so throwing always represents a fatal error.