C++ asynchronous callback function reference passing null pointer exception

Problem description

A recent desktop application developed using c++ / qt failed to run to a method that asynchronously executed python script tasks:

Process ended, exit code -1073741819 (0xC0000005)Copy the code

When a python script is executed asynchronously by a separate thread, the callback from the UI thread returns the result to the UI thread.

void TestCaseProject::initProTestCasesEnvAsync(const std::function<void(std::vector<std::pair<std::string, Json::Value>>)>& _callback) {
    std::thread t{[&] () {
        doWithSetRunning([&] () {auto result = this->initProTestCasesEnv(a); _callback(result); }); }}; t.detach(a); }void callBackFunc(const std::vector<std::pair<std::string, Json::Value>>& rps) {
    // do some things
}

// A UI thread function called initProTestCasesEnvAsync
void uiFunc(a) {
    // create project and other code
    project->initProTestCasesEnvAsync(callBackFunc);
    // do other things
}
Copy the code

The solution

Problem reading

The search process has ended and the exit code is -1073741819 (0xC0000005).

  • The exit code is the return value of the executed program when it exits. For example, the main function returns directly, the void exit(int _Code) function that calls the program to exit, and the system defines the error exit code when an unsolved exception is thrown from the program to the system.

  • The parentheses in the exit code are the actual hexadecimal exit codes (usually used), followed by their decimal representation (negative because the hexadecimal number c starts with it, as a sort of identifier to distinguish between the series of error codes).

  • Error code cannot determine the details of the error, but can only be roughly judged. Specific cases need to be further analyzed in the code context, or catch exceptions and debug to determine.

  • Error exit codes are typically triggered by an unhandled exception, rather than simply exiting the program and returning the code.

  • On Windows, the error code is defined in the header

    For details about Windows error codes, see the official documentation. At the same time, Microsoft has provided an official error code lookup tool with a download link.

  • Other operating systems also have error code locations, but the locations may be different, so you can find them yourself. However, error codes and their meanings are basically the same across platforms and do not differ much.

Problem analysis

1. Error code analysis

Use the Microsoft Error Code Finder tool to find the error code 0xC0000005 as follows:

PS D: \ tools >. \ Err_6. 4.5. Exe C0000005# for hex 0xc0000005 / decimal -1073741819

  ISCSI_ERR_SETUP_NETWORK_NODE                                   iscsilog.h
# Failed to setup initiator portal. Error status is given in
# the dump data.

  STATUS_ACCESS_VIOLATION                                        ntstatus.h
# The instruction at 0x%p referenced memory at 0x%p. The
# memory could not be %s.

  USBD_STATUS_DEV_NOT_RESPONDING                                 usb.h
# as an HRESULT: Severity: FAILURE (1), FACILITY_NONE (0x0), Code 0x5
# for hex 0x5 / decimal 5

  WINBIO_FP_TOO_FAST                                             winbio_err.h
# Move your finger more slowly on the fingerprint reader.
# as an HRESULT: Severity: FAILURE (1), FACILITY_NULL (0x0), Code 0x5

  ERROR_ACCESS_DENIED                                            winerror.h
# Access is denied.

# 5 matches found for "C0000005"
Copy the code

Upon analysis, the fifth search result (line 20) is the main cause of the problem (see the code defined in

).

ERROR_ACCESS_DENIED: Access is denied. Access is denied, which means access to the unauthorized memory address space. Common scenarios are as follows:

  • Null pointer
  • An array
  • Wild pointer generated after freeing memory

All of the above scenarios result in undefined behavior and may throw an exception that triggers ERROR_ACCESS_DENIED and exit.

2. Code debugging

Running in debug mode, the following exception information is displayed:

terminate called after throwing an instance of 'std::bad_function_call'
  what():  bad_function_call
Copy the code

The STD ::bad_function_call exception is thrown when an empty function object (STD ::function) is called. An empty function object is usually either unassigned or null.

The function object of the callback function was a function in the UI mainline that passed Pointers to the global function to the constructor. The argument to the initProTestCasesEnvAsync method was a constant reference that was captured by the thread-executing lambda function. The reference is captured by the lambda function argument of the doWithSetRunning in the thread-executed function, and the function object is called within it.

After a single line of debugging, it was discovered that the exception was raised when the callback object was executed on an asynchronous thread.

As savvy protestors may have figured out, according to the variable pass-through relationship described above, the final callback function object executed was a reference to a local function object that was passed the callBackFunc function pointer and constructed when the UI thread called initProTestCasesEnvAsync. A normal serialized protestants program should not be a problem. The callBackFunc was completed when initProTestCasesEnvAsync returned. However, if the callback object is created on a different thread from the one that executed it, the local callback object will free up memory because the function in its context finishes asynchronously, resulting in a null pointer inside the reference to the callback held by the executing thread, raising an STD :: BAD_function_call exception when called.

Problem solving

Once you know the problem, it’s easy to fix it. An asynchronous thread, in addition to the global, dynamically allocated memory and other non-local objects can share memory data for reading and writing, local data must be copied to achieve isolation. Based on the above theory, there should be a copy of local variables passed in by the UI thread call, and a direct reference to dynamically requested objects. Since only the this pointer and passed function arguments are used in the body of the actual code executed, copy them both.

In the task thread’s lambda execution function, change the capture reference to the capture value, and the internal doWithSetRunning lambda execution function captures the reference to the copy that the asynchronous thread captures. You can isolate the sharing of non-local variables (the memory object to which this points) from local variables (the _callback callback function object).

Modify the code as follows:

void TestCaseProject::initProTestCasesEnvAsync(const std::function<void(std::vector<std::pair<std::string, Json::Value>>)>& _callback) {
    std::thread t{[=] () {
        doWithSetRunning([&] () {auto result = this->initProTestCasesEnv(a); _callback(result); }); }}; t.detach(a); }Copy the code

Finally, we must pay attention to the correctness of the lambda reference pass, because the editor has encountered many problems here, and in the asynchronous scenario, it is more important to pay attention to the transfer relationship and life cycle of each object in the object pass process.