Search code examples
c++smart-pointersdbus

std::unique_ptr<DBusMessage> != nullptr after move into function parameter


I'm trying to write a program that uses DBus to interact with BlueZ. From the information I have been able to gather from the DBus docs, BlueZ DBus API docs, and various forums, my thought process was as follows:

  1. Connect to DBus
  2. Start discovery using the "default" adapter (just hard-coded to hci0)
  3. Use org.freedesktop.DBus.ObjectManager.GetManagedObjects() to find available devices.

So I have wrote the following code:

#include <...>
#include <dbus-1.0/dbus/dbus.h>

struct DBusDeleter
{
    void operator()(DBusConnection *connection) {dbus_connection_unref(connection);}
    void operator()(DBusMessage *msg) {dbus_message_unref(msg);}
    void operator()(DBusError *err) {dbus_error_free(err);}
};

void print_managed_objects(std::unique_ptr<DBusMessage, DBusDeleter> message)
{
    DBusMessageIter message_iter;
    dbus_message_iter_init(message.get(), &message_iter);
    std::cout << (char) dbus_message_iter_get_arg_type(&message_iter) << '\n';

    DBusMessageIter dict_iter;
    dbus_message_iter_recurse(&message_iter, &dict_iter);
    std::cout << (char) dbus_message_iter_get_arg_type(&dict_iter) << '\n';

    while(dbus_message_iter_get_arg_type(&dict_iter) == DBUS_TYPE_DICT_ENTRY)
    {
        DBusMessageIter entry_iter;
        dbus_message_iter_recurse(&dict_iter, &entry_iter);

        char * object_path;

        dbus_message_iter_get_basic(&entry_iter, object_path);
        std::cout << object_path << "\n";

        std::cout << (char) dbus_message_iter_get_arg_type(&entry_iter) << '\n';

        dbus_message_iter_next(&dict_iter);
    }
    std::cout << std::flush;
}

int main()
{
    std::unique_ptr<DBusError, DBusDeleter> dbus_error(new DBusError);
    dbus_error_init(dbus_error.get());

    std::shared_ptr<DBusConnection> conn(dbus_bus_get(DBUS_BUS_SYSTEM, dbus_error.get()), DBusDeleter());

    if (dbus_error_is_set(dbus_error.get()))
    {
        std::cout << "DBus Error: " << dbus_error->message << std::endl;
        return 1;
    }

    std::string service_name = "org.bluez";
    std::unique_ptr<DBusMessage, DBusDeleter> msg, reply;

    // Start Device Discovery

    msg.reset(dbus_message_new_method_call(service_name.c_str(), "/org/bluez/hci0", "org.bluez.Adapter1", "StartDiscovery"));
    reply.reset(nullptr);

    uint32_t serial = 1;

    dbus_connection_send(conn.get(), msg.get(), &serial);

    if (dbus_error_is_set(dbus_error.get()))
    {
        std::cout << "BlueZ Error (Failed to start discovery): " << dbus_error->message << std::endl;
        return 1;
    }

    std::chrono::duration<int64_t> scan_duration = std::chrono::seconds(10);
    std::chrono::_V2::system_clock::time_point start_time = std::chrono::system_clock::now();

    msg.reset(dbus_message_new_method_call(service_name.c_str(), "/", "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"));

    while (start_time + scan_duration >= std::chrono::system_clock::now())
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(500));

        reply = std::unique_ptr<DBusMessage, DBusDeleter>(dbus_connection_send_with_reply_and_block(conn.get(), msg.get(), -1, dbus_error.get()));

        if (dbus_error_is_set(dbus_error.get()))
        {
            std::cout << "error with reply: " << dbus_error->message << "\n";
            break;
        }

        print_managed_objects(std::move(reply));
        
        if (reply == nullptr) {
            std::cout << "All Good!\n";
            continue;
        }

        std::cout << "Something went wrong!\n";
        break;
    }

    // more code...
}

The issue happens right around the print_managed_objects function call. When the function returns, I would expect that since the print_managed_objects function has already consumed the message as I had gave it ownership of the message, that everything would be okay, but the check for nullptr fails on the original unique_ptr that was moved from.

This means that the reply pointer still has a reference to the message which was destroyed after print_managed_objects returned, and when resetting the pointer in the next iteration of the loop, the program would crash because DBus would spit out this error:

dbus[15920]: arguments to dbus_message_unref() were incorrect, assertion "message->generation == _dbus_current_generation" failed in file dbus-message.c line 1728

My question is why is reply's internal pointer not set to nullptr after the move? According to another Stack Overflow question, calling std::move() on a unique pointer will set that pointer's internal pointer to nullptr.

Edit: I had left out lines in the print_managed_objects function that I thought I could leave out for a minimal reproducible example, but those lines were actually necessary for the error to occur. Specifically, these lines:

char * object_path;

dbus_message_iter_get_basic(&entry_iter, object_path);
std::cout << *object_path << "\n";

Solution

  • Here is an issue.

    char * object_path;
    dbus_message_iter_get_basic(&entry_iter, object_path);
    std::cout << *object_path << "\n";
    

    You pass the uninitialized pointer to the function. This is undefined behavior. You seem to get corrupted stack data in your case.

    std::cout << *object_path << "\n";
    

    I don't know what you expect there. As for the 3rd line, you likely expect a single char data. The code should be

    char object_path;
    dbus_message_iter_get_basic(&entry_iter, &object_path);
    std::cout << object_path << "\n";
    

    &object_path passes the address of the char storage.

    The top code is showing

    std::cout << object_path << "\n"
    

    So, I am confused by your code.

    A similar example is shown in the manual. You could easy follow it instead of trials. dbus_message_iter_get_basic().