Mixed Call Stacks with C# and C++
C# is a common language for creating cross-platform applications and user interfaces. In many cases, C# and C++ are used together to create more sophisticated applications. In some scenarios, a C# application will make calls to functions in managed or unmanaged C++ libraries. In others, a C++ application may act as the shell for the app, and cross-platform C# UI code is used in it. In either case, engineers require detailed crash and exception report data from both the C# and C++ applications to effectively debug and identify the root cause of any faulty code.
Backtrace provides exception reporting for C# applications using our Backtrace C# reporting library and crash reporting for C++ applications using the Open Source Crashpad or Breakpad libraries.
This document will discuss how to configure a Windows-based application using Backtrace's fork of the Crashpad library and the C# reporting library to enable crash and exception reporting. The solution includes the ability to relate the call stacks from the two languages together using a custom attribute called process_id
. The steps this document will outline include:
- Description and UML Activity Diagram of Flow Control between C# and C++
- How to set up Crash and Exception Reporting in C# and C++
- Sample code and custom attributes to include in Crash and Exception Reports
For non-Windows-based environments, reach out to our support team via the in-app chat on the bottom right of your screen.
What You'll Need
- A Backtrace account (log in or sign up for a free trial license).
- Your subdomain name (used to connect to your Backtrace instance). For example,
https://example-subdomain.sp.backtrace.io
. - A Backtrace project and a submission token.
Description And UML Activity Diagram of Flow Control Between C# And C++
In scenarios where a C# or C++ application calls unmanaged C++ code or a C# method, and that code throws an exception, the crash reporting platform needs to ensure that call stacks and other relevant data from both portions of code are available for analysis.
The figure below presents UML Activity diagrams depicting how the code is executed and what information is provided to identify the calling and failing code.
The C# environment requires a callback method, invoked by the C++ environment, to allow the developer to send additional data from the C# application state together with C++ report generated by Crashpad. The system also requires that an additional attribute - process_id
- is added to the Crashpad and Backtrace C# Report to identify both parts of the call stack.
The remainder of this document will discuss how you can configure your application to report exceptions and crashes to your Backtrace instance. Then, you will find sample code showing how to.
How to Setup Crash And Exception Reporting in C# And C++
Configuring your application environments to report Crashes and Exceptions is the first step:
- Ensure that you have created a Backtrace instance and obtained a submission token for your project. Both C# and C++ crash reports should be sent to the same project.
- Review the Readme for the Backtrace C# reporting library. Follow the instructions to download and configure the library. Also, review the options for offline storage and submission of crash reports. Integrate the Backtrace C# library into your C# applications.
- Read the Crashpad Integration Guide and follow the instructions to use Backtrace's Crashpad binaries. You can download the binaries and view the source code on the Backtrace branch on Github. These binaries include additional features not available in the community-maintained (master) branch. Specifically, you can attach files to C++ crash reports, and generate a minidump even when a crash doesn't occur. This is useful for mixed call stacks because most C++ functions called from C# include exception handling code to allow the system to recover. However, engineers still need a minidump file to understand the call stack and investigate the root cause. Refer to the "Send reports using EXCEPTION_POINTERS in Windows" section in the Readme for more details.
Sample Code And Custom Attributes to Include in Crash And Exception Reports
After configuring the Backtrace C# reporting library and Crashpad, use the following sample code to determine when to generate reports and how to include the appropriate process_id
attribute.
Invoking C++ Methods from C# Code
In C#, you can invoke methods from unmanaged C++ libraries. To set up a custom callback and capture all exceptions from the C++ library, follow these steps:
To invoke the exposed method from the C++ library, you have to use the DllImport
attribute before the method definition. DllImport
requires at least one attribute, the path to the .dll file. The method definition in C# requires the use of the special keyword "extern". The C# and C++ method names should be the same. For example, if you expose a method with the name captureVideo
from C++, you have to create an extern captureVideo
method in your C# code. See the sample code below with the method name CrashApp
:
[DllImport(@"path to dll file", CallingConvention = CallingConvention.ThisCall)]
static extern void CrashApp(LogBuffer g_logger);
In our example, we want to set up a callback function. To prepare the callback, we create a delegate method with different types of parameters: IntPtr
, int
, and int
. In our case, we use the callback function to invoke C# code before the Crashpad DumpWithoutCrash
method. You can check the delegate declaration and usage below:
delegate void LogBuffer(IntPtr buf, int len, int flags);
private LogBuffer logger;
private BacktraceClient client;
public void OnLogMessage(IntPtr _buf, int len, int flags)
{
if (len == 0 || _buf == IntPtr.Zero)
{
Console.WriteLine("Buffer or len is empty.");
}
byte[] managedArray = new byte[len - 1];
Marshal.Copy(_buf, managedArray, 0, len - 1);
Console.WriteLine(managedArray);
string result = Encoding.UTF8.GetString(managedArray);
Console.WriteLine(result);
var report = new BacktraceReport(
message: "Event caught by C#",
attributes: new Dictionary<string, object>() {
{ "process.id", result }
}
);
Task.WaitAll(client.SendAsync(report));
}
public void ExecuteTasks()
{
logger = OnLogMessage;
CrashApp(logger);
}
Execute C++ Methods
If you want to run C++ library code from C#, use the following code to export your method:
extern "C" _declspec(dllexport) methodType MethodName(method args...) {}
The following is a more detailed implementation that allows you to pass a callback function to C#. (Check the Invoking C++ Methods from C# Code section above to read more about callback functions.)
extern "C" _declspec(dllexport) void CrashApp(LogBuffer g_logger) {
LogBuffer buffer = g_logger;
BacktraceClient client = BacktraceClient::BacktraceClient();
if (buffer != IntPtr.Zero) {
client.SetupLogger(buffer);
}
client.Crash();
}
C++ Calling Into C#
In the following scenario, a C# application is hosted by a managed C++ application. In this situation, we suggest adding the UnhandledApplicationException
handler provided by the Backtrace C# library and the try/catch
block. The C# application can send a report inside the catch
block via BacktraceClient
and rethrow the exception. If the C# application rethrows the exception, the C++ handler and Crashpad will catch the exception inside the __try
__except
block and send additional information about the current application state to Backtrace.
C++ Application that hosts the C# Code: In this example,
- The C++ code creates a new AboutForm window using the C# WPF application.
- The constructor requires the processId generated by the C++ library.
- The method SetNumber invokes the method where C# will crash.
- The C# library throws an ArgumentException for the Number parameter equal to 6.
- When C# throws an exception, both C# and C++ exception information is required by the C#/C++ developer.
- We use a try/catch block to catch the exception inside the method and send the exception object to Backtrace.
- The C# catch block rethrows the exception to C++, so the __except block will receive EXCEPTION_POINTERS information and send the data via Crashpad to the Backtrace API. This solution doesn't require any additional callback methods.
CPPCLIInterop::CPPCLIInterop(char processId[37])
{
// Initialize C# win forms and convert the char array processId to a string
System::String^ id = gcnew System::String(processId);
pForm1 = gcnew AboutForm(id);
}
CPPCLIInterop::~CPPCLIInterop()
{ }
void CPPCLIInterop::Show()
{
pForm1->Show();
}
void CPPCLIInterop::SetNumber(int Number)
{
EXCEPTION_POINTERS* pointer = NULL;
__try {
// Method where C# throws an exception
pForm1->SetNumber(Number);
}
__except (LogExceptionPointer(pointer = GetExceptionInformation())) {
// Exception handler code
std::exit(1);
}
}
C# Application:
public AboutForm(string processId)
{
InitializeBacktrace(processId);
InitializeComponent();
}
private void ValidateNumber(int number)
{
if (number == 6) throw new ArgumentException(nameof(number));
}
public void SetNumber(int Number)
{
try
{
ValidateNumber(Number);
textBox1.Text = Number.ToString();
}
catch (Exception e)
{
_client.Send(e);
throw;
}
}
private void InitializeBacktrace(string processId)
{
var credentials = new BacktraceCredentials(_host, _appToken);
var clientConf = new BacktraceClientConfiguration(credentials);
var db = new BacktraceDatabase(_databasePath);
_client = new BacktraceClient(clientConf, db);
_client.Attributes.Add("process_id", processId);
}
Viewing the C++ and C# Reports in Backtrace
After generating and submitting C# and C++ exception and crash reports, you can view them in the Backtrace client. The quickest way to see the incoming reports is a table view. You can choose to list the timestamp, application, error message, and call stack, among other attributes. By doing so, you can see reports from C# and C++ components side by side.