Viewing user defined types
We can also view the contents of a user defined type. In the simple script code in this project, we have created a C# type named Important with a
field named InstanceIdentifier. If I set a breakpoint just after we create the second instance of the Important type in the script,
I can see that the generated code has set InstanceIdentifier to a value of 1, as expected.
So viewing the contents of user defined types in generated code is done that same way as you normally would in C++ code in Xcode.
Breaking on exceptions in generated code
Often I find myself debugging generated code to try to track down the cause of a bug. In many cases these bugs are manifested
as managed exceptions. As we discussed in the last post, IL2CPP uses C++ exceptions to implement managed exceptions, so we can break
when a managed exception occurs in Xcode in a few ways.
The easiest way to break when a managed exception is thrown is to set a breakpoint on the il2cpp_codegen_raise_exception function,
which is used by il2cpp.exe any place where a managed exception is explicitly thrown.
If I then let the project run, Xcode will break when the code in Start throws an InvalidOperationException exception.
This is a place where viewing string content can be very useful. If I dig into the members of the ex argument,
I can see that it has a ___message_2 member, which is a string representing the message of the exception.
With a little bit of fiddling, we can print the value of this string and see what the problem is:
(lldb) p il2cpp::utils::StringUtils::Utf16ToUtf8(&ex->___message_2->___start_char_1)
(std::__1::string) $88 = "Don't panic"
Note that the string here has the same layout as above, but the names of the generated fields are slightly different.
The chars field is named ___start_char_1 and its type is uint16_t, not uint16_t. It is still the first character of
an array though, so we can pass its address to the conversion function, and we find that the message in
this exception is rather comforting.
But not all managed exceptions are explicitly thrown by generated code. The libil2cpp runtime code will throw managed
exceptions in some cases, and it does not call il2cpp_codegen_raise_exception to do so. How can we catch these exceptions?
If we use Debug > Breakpoints > Create Exception Breakpoint in Xcode, then edit the breakpoint, we can choose C++
exceptions and break when an exception of type Il2CppExceptionWrapper is thrown. Since this C++ type is used to wrap all managed
exceptions, it will allow us to catch all managed exceptions.
Let’s prove this works by adding the following two lines of code to the top of the Start method in our script:
Important boom = null;
The second line here will cause a NullReferenceException to be thrown. If we run this code in Xcode with the
exception breakpoint set, we’ll see that Xcode will indeed break when the exception is thrown. However, the breakpoint
is in code in libil2cpp, so all we see is assembly code. If we take a look at the call stack, we can see that we need
to move up a few frames to the NullCheck method, which is injected by il2cpp.exe into the generated code.
From there, we can move back up one more frame, and see that our instance of the Important type does indeed have a value of NULL.
After discussing a few tips for debugging generated code, I hope that you have a better understanding about how to
track down possible problems using the C++ code generated by IL2CPP. I encourage you to investigate the layout of other types
used by IL2CPP to learn more about how to debug the generated code.
Where is the IL2CPP managed code debugger though? Shouldn’t we be able to debug managed code running
via the IL2CPP scripting backend on a device? In fact, this is possible. We have an internal, alpha-quality
managed code debugger for IL2CPP now. It’s not ready for release yet, but it is on our roadmap, so stay tuned.
The next post in this series will investigate the different ways the IL2CPP scripting backend implements various types
of method invocations present in managed code. We will look at the runtime cost of each type of method invocation.