Search code examples
c++asynchronousboostgrpc

Is it possible to use a smart ptr or boost intrusive ptr as the "void* tag" value in a gRPC async server written in C++


I'm writing an async gRPC server in C++ (on windows). I'd like to use the boost intrusive pointer type for the 'tag' value - the pointer to the RPC handler objects which are returned during the completion queue 'Next()' method.

The gRPC async service requires passing a void* to the handler object so it can call the handler when the associated event occurs. The problem is that I'm unable to find a way to convert my boost intrusive pointer to a void* in a way that preserves and uses the reference count.

Is it possible? Or would it only work if the method I pass the pointer to expects a boost pointer?


Solution

  • Let's say we have a third party library that takes static callback functions, which can use a void* userdata to pass user-defined state around:

    namespace SomeAPI {
        typedef void(*Callback)(int, void* userdata);
    
        struct Registration;
    
        Registration const* register_callback(Callback cb, void* userdata);
        size_t deregister_callback(Callback cb, void* userdata);
        void some_operation_invoking_callbacks();
    }
    

    A minimalist implementation of this fake API is e.g.:

    struct Registration {
        Callback cb;
        void* userdata;
    };
    std::list<Registration> s_callbacks;
    
    Registration const* register_callback(Callback cb, void* userdata) {
        s_callbacks.push_back({cb, userdata});
        return &s_callbacks.back();
    }
    
    size_t deregister_callback(Callback cb, void* userdata) {
        auto oldsize = s_callbacks.size(); // c++20 makes this unnecessary
        s_callbacks.remove_if([&](Registration const& r) {
            return std::tie(r.cb, r.userdata) == std::tie(cb, userdata);
        });
        return oldsize - s_callbacks.size();
    }
    
    void some_operation_invoking_callbacks() {
        static int s_counter = 0;
        for (auto& reg : s_callbacks) {
            reg.cb(++s_counter, reg.userdata);
        }
    }
    

    Let's Have A Client

    The Client owns state which is managed by some shared pointer:

    struct MyClient {
        struct State {
            std::string greeting;
    
            void foo(int i) {
                std::cout
                    << "State::foo with i:" << i
                    << " and greeting:" << std::quoted(greeting)
                    << "\n";
            }
        };
        using SharedState = std::shared_ptr<State>;
        SharedState state_;
    

    Now, we want the State::foo member to be registered as a callback, and the state_ should be passed as the user-data:

        MyClient(std::string g) : state_(std::make_shared<State>(State{g})) {
            SomeAPI::register_callback(static_handler, &state_);
        }
    
        ~MyClient() noexcept {
            SomeAPI::deregister_callback(static_handler, &state_);
        }
    
        static void static_handler(int i, void* userdata) {
            auto& state = *static_cast<SharedState*>(userdata);
            state->foo(i);
        }
    };
    

    Now to exercise the Client a bit:

    Live On Coliru

    int main() {
        MyClient client1("Foo");
    
        std::cout << " ------- operation start\n";
        SomeAPI::some_operation_invoking_callbacks();
    
        {
            MyClient client2("Bar");
            std::cout << " ------- operation start\n";
            SomeAPI::some_operation_invoking_callbacks();
        }
    
        std::cout << " ------- operation start\n";
        SomeAPI::some_operation_invoking_callbacks();
    }
    

    Prints:

     ------- operation start
    State::foo with i:1 and greeting:"Foo"
     ------- operation start
    State::foo with i:2 and greeting:"Foo"
    State::foo with i:3 and greeting:"Bar"
     ------- operation start
    State::foo with i:4 and greeting:"Foo"
    

    Tight-Rope Act

    If you actually want to pass ownership to the API, in the sense that it keeps the state around even if the Client instance is gone, you will, by definition, leak that state.

    The only solution to this would be if the API had some kind of callback to signal that it should cleanup up. I've never seen such an API design, but here is what it would look like for this:

    enum { TEARDOWN_MAGIC_VALUE = -1 };
    void InitAPI() {}
    void ShutdownAPI() {
        for (auto it = s_callbacks.begin(); it != s_callbacks.end();) {
            it->cb(TEARDOWN_MAGIC_VALUE, it->userdata);
            it = s_callbacks.erase(it);
        }
    }
    

    Now we can pass a dynamically allocated copy of the shared_ptr to the call-back as user-data (instead of a raw pointer to the owned copy of the shared pointer):

    SomeAPI::register_callback(static_handler, new SharedState(state_));
    

    Note that because SomeAPI now has a copy of the shared-pointer, the refcount has increaed. In fact, we don't have to deregister the callback anymore because the State will stay valid, until SomeAPI actually shuts down:

    static void static_handler(int i, void* userdata) {
        auto* sharedstate = static_cast<SharedState*>(userdata);
        if (i == SomeAPI::TEARDOWN_MAGIC_VALUE) {
            delete sharedstate; // decreases refcount
            return;
        } else {
            (*sharedstate)->foo(i);
        }
    }
    

    The main program is basically un-altered, but for InitAPI() and ShutdownAPI() calls:

    int main() {
        SomeAPI::InitAPI();
    
        MyClient client1("Foo");
    
        std::cout << " ------- operation start\n";
        SomeAPI::some_operation_invoking_callbacks();
    
        {
            MyClient client2("Bar");
            std::cout << " ------- operation start\n";
            SomeAPI::some_operation_invoking_callbacks();
        }
    
        std::cout << " ------- operation start\n";
        SomeAPI::some_operation_invoking_callbacks();
    
        SomeAPI::ShutdownAPI();
    }
    

    Through some clever tracing of destructors, you can verify that lifetime of State is now actually governed/shared by ShutdownAPI:

    Live On Coliru

     ------- operation start
    State::foo with i:1 and greeting:"Foo"
     ------- operation start
    State::foo with i:2 and greeting:"Foo"
    State::foo with i:3 and greeting:"Bar"
    ~MyClient (Bar)
     ------- operation start
    State::foo with i:4 and greeting:"Foo"
    State::foo with i:5 and greeting:"Bar"
    ~State (Bar)
    ~MyClient (Foo)
    ~State (Foo)
    

    Full Listing (Classic)

    Live On Coliru

    #include <memory>
    #include <list>
    #include <string>
    #include <iostream>
    #include <iomanip>
    
    namespace SomeAPI {
        using Callback = void(*)(int, void* userdata);
    
        struct Registration {
            Callback cb;
            void* userdata;
        };
        std::list<Registration> s_callbacks;
    
        Registration const* register_callback(Callback cb, void* userdata) {
            s_callbacks.push_back({cb, userdata});
            return &s_callbacks.back();
        }
    
        size_t deregister_callback(Callback cb, void* userdata) {
            auto oldsize = s_callbacks.size(); // c++20 makes this unnecessary
            s_callbacks.remove_if([&](Registration const& r) {
                return std::tie(r.cb, r.userdata) == std::tie(cb, userdata);
            });
            return oldsize - s_callbacks.size();
        }
    
        void some_operation_invoking_callbacks() {
            static int s_counter = 0;
            for (auto& reg : s_callbacks) {
                reg.cb(++s_counter, reg.userdata);
            }
        }
    }
    
    struct MyClient {
        struct State {
            std::string greeting;
    
            void foo(int i) {
                std::cout
                    << "State::foo with i:" << i
                    << " and greeting:" << std::quoted(greeting)
                    << "\n";
            }
        };
        using SharedState = std::shared_ptr<State>;
        SharedState state_;
    
        MyClient(std::string g) : state_(std::make_shared<State>(State{g})) {
            SomeAPI::register_callback(static_handler, &state_);
        }
    
        ~MyClient() noexcept {
            SomeAPI::deregister_callback(static_handler, &state_);
        }
    
        static void static_handler(int i, void* userdata) {
            auto& state = *static_cast<SharedState*>(userdata);
            state->foo(i);
        }
    };
    
    int main() {
        MyClient client1("Foo");
    
        std::cout << " ------- operation start\n";
        SomeAPI::some_operation_invoking_callbacks();
    
        {
            MyClient client2("Bar");
            std::cout << " ------- operation start\n";
            SomeAPI::some_operation_invoking_callbacks();
        }
    
        std::cout << " ------- operation start\n";
        SomeAPI::some_operation_invoking_callbacks();
    }
    

    Full Listing (Contrived)

    Live On Coliru

    #include <memory>
    #include <list>
    #include <string>
    #include <iostream>
    #include <iomanip>
    
    namespace SomeAPI {
        enum { TEARDOWN_MAGIC_VALUE = -1 };
        using Callback = void(*)(int, void* userdata);
    
        struct Registration {
            Callback cb;
            void* userdata;
        };
        std::list<Registration> s_callbacks;
    
        Registration const* register_callback(Callback cb, void* userdata) {
            s_callbacks.push_back({cb, userdata});
            return &s_callbacks.back();
        }
    
        size_t deregister_callback(Callback cb, void* userdata) {
            auto oldsize = s_callbacks.size(); // c++20 makes this unnecessary
            s_callbacks.remove_if([&](Registration const& r) {
                bool const matched = std::tie(r.cb, r.userdata) == std::tie(cb, userdata);
                if (matched) {
                    r.cb(TEARDOWN_MAGIC_VALUE, r.userdata);
                }
                return matched;
            });
            return oldsize - s_callbacks.size();
        }
    
        void some_operation_invoking_callbacks() {
            static int s_counter = 0;
            for (auto& reg : s_callbacks) {
                reg.cb(++s_counter, reg.userdata);
            }
        }
    
        void InitAPI() {}
        void ShutdownAPI() {
            for (auto it = s_callbacks.begin(); it != s_callbacks.end();) {
                it->cb(TEARDOWN_MAGIC_VALUE, it->userdata);
                it = s_callbacks.erase(it);
            }
        }
    }
    
    struct MyClient {
        struct State {
            std::string greeting;
            State(std::string g) : greeting(std::move(g)) {}
    
            void foo(int i) {
                std::cout
                    << "State::foo with i:" << i
                    << " and greeting:" << std::quoted(greeting)
                    << "\n";
            }
    
            ~State() noexcept {
                std::cout << "~State (" << greeting << ")\n";
            }
        };
        using SharedState = std::shared_ptr<State>;
        SharedState state_;
    
        MyClient(std::string g) : state_(std::make_shared<State>(std::move(g))) {
            SomeAPI::register_callback(static_handler, new SharedState(state_));
        }
    
        ~MyClient() {
            std::cout << "~MyClient (" << state_->greeting << ")\n";
        }
    
        static void static_handler(int i, void* userdata) {
            auto* sharedstate = static_cast<SharedState*>(userdata);
            if (i == SomeAPI::TEARDOWN_MAGIC_VALUE) {
                delete sharedstate; // decreases refcount
                return;
            } else {
                (*sharedstate)->foo(i);
            }
        }
    };
    
    int main() {
        SomeAPI::InitAPI();
    
        MyClient client1("Foo");
    
        std::cout << " ------- operation start\n";
        SomeAPI::some_operation_invoking_callbacks();
    
        {
            MyClient client2("Bar");
            std::cout << " ------- operation start\n";
            SomeAPI::some_operation_invoking_callbacks();
        }
    
        std::cout << " ------- operation start\n";
        SomeAPI::some_operation_invoking_callbacks();
    
        SomeAPI::ShutdownAPI();
    }