Overview

C# is a common language used to create cross-platform applications and user interfaces. In many cases, C# and C++ will be used together to create a more sophisticated application. In some of those cases, a C# application will make calls to functions within 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 within. Either way, engineers require detailed crash and exception report data from both the C# and C++ applications to effectively debug and identify the root cause for the 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:

  1. Description and UML Activity Diagram of Flow Control between C# and C++
  2. How to setup Crash and Exception Reporting in C# and C++
  3. Sample code and custom attributes to include in Crash and Exception Reports

For non-Windows based environments, please reach out to our support team via the in-app chat on the bottom right of your screen.

Description and UML Activity Diagram of Flow Control between C# and C++

In the case 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 from generated by Crashpad. The system also requires that an additional attribute - process_id - is added to the Crashpad and Backtrace C# Report in order to identify both parts of the call stack. 

The remainder of this document will discuss how you can configure your application report on exceptions and crashes to your Backtrace instance. Then, you will find sample code showing how to generate the exception and crash reports with the same process_id.

How to setup Crash and Exception Reporting in C# and C++

Configuring your application environments to report Crashes and Exceptions is the first step:

  1. Make sure you have a Backtrace instance created and a submission token for your project. C# and C++ crash reports should be sent to the same project. You can find more information in the Best Practices for Integrating your project with Backtrace
  2. Review the Readme for the Backtrace C# reporting library. Follow the instructions to get and configure the library, and review the options to use for offline storage and submission of crash reports. Integrate the Backtrace C# library into your C# applications.
  3. Review the Crashpad Integration Guide and follow the instructions to use Backtrace's binaries for Crashpad (download binaries, view source on Backtrace branch on Github) These binaries include some features not available in the community maintained (master) branch. Specifically, you can attach files to C++ crash reports, and you can cause a minidump to be generated even when a crash doesn't occur. This is useful for mixed call stacks as most C++ functions called from C# will include exception handling code that allows the system to recover, but your engineers will still want to see a minidump file so they can understand the call stack and investigate the root cause. See "Send reports using EXCEPTION_POINTERS in Windows" in the Readme.

Sample code and custom attributes to include in Crash and Exception Reports

After you configure the Backtrace C# reporting library and Crashpad, follow the sample code below to see when to generate reports and the appropriate process_id attribute.

Invoke C++ methods from C# code

C# allows you to invoke methods from unmanaged C++ libraries. In doing so, you want to setup a custom callback and get all exceptions from the C++ library. 

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. (i.e If you expose method with name captureVideo from C++ you have to create extern captureVideo method in your C# code). See 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 setup a callback function. To prepare the callback we create delegate method with different type of parameters - IntPtr, int, int. In our case we use the callback function to invoke C# code before Crashpad DumpWithoutCrash method. You can check 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 == null)
    {
        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 catch 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 execute C++ library code from C# use following code to export your method:

extern "C" _declspec(dllexport) methodType MethodName(method args…) {}

Following is a more detailed implementation that allows you to pass callback function to C# (Check “Invoke C++ methods from C# code” above section 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 != NULL) {
client.SetupLogger(buffer);
}
client.Crash();
}

 C++ to call into C#

In the following scenario, a C# application is hosted by a managed C++ application. In this situation we suggest to add the UnhandleApplicationException 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 exception. If the C# application rethrows the exception, the C++ handler and Crashpad will catch exception inside the __try __expect block, and send additional information about current application state to Backtrace.

C++ Application that hosts the C# Code: In this example, 

  • C++ code creates a new AboutForm window by using C# WPF application. 
  • The constructor requires processId generated by C++ library. 
  • The method SetNumber invokes the method where C# will crash. 
  • C# library throws ArgumentException for Number parameter equals to 6. 
  • When C# throws an exception, the C#/C++ developer requires both C# and C++ exception information. 
  • We use try/catch block to catch exception inside the method and send exception object to Backtrace. 
  • C# catch block rethrows the exception to C++, so __except block will receive EXCEPTION_POINTERS information and send data via Crashpad to Backtrace API. This solution don’t require any additional callback methods.  
CPPCLIInterop::CPPCLIInterop(char processId[37])
{
//initialize C# win forms and convert char array processId to 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# throw 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. As seen in the figure below, you can choose to list the timestamp, application, error message and callstack, amongst other attributes. By doing so, you can see reports from C# and C++ components side by side.

In the video above, you can see crashes and exceptions being reported in a single project, and you can see the difference in reports based on the application name. The NativeInteropApp.exe and DotNetInvoke.exe apps are C++ based, and you can see the CSharp application is C# based. You can also see process_id attribute that can tie the 2 different call stacks together.

Did this answer your question?