2

I wish to share objects containing internal pointers between unrelated processes on Linux.

Similar to this question?

Does boost.interprocess support this?

Any data structure containing raw pointers cannot be used for IPC unless each process is somehow able to map the memory to the same location.

What is boost's answer to this?

Bruce Adams
  • 4,953
  • 4
  • 48
  • 111

2 Answers2

3

If the address space is shared it is safe to use such an allocator for the complete structure

You're mixing "shared address space" and "address mapped at the same address". Because if there was no sharing of address space, there would also not be an issue storing raw pointers.

What is important is to use fancy pointers like boost::interprocess::offset_ptr<T>. The interprocess allocator uses offset_ptr<T>.

This is why you need to use container implementations that support fancy pointers.


You're very close. The canonical way to do what you sketch would look like this:

Live On Coliru

#include <boost/container/string.hpp>
#include <boost/container/vector.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/managed_mapped_file.hpp>
#include <string>
namespace bip = boost::interprocess;

namespace Shared {
#ifdef COLIRU
    using Segment = bip::managed_mapped_file;
#else
    using Segment = bip::managed_shared_memory;
#endif
    using Mgr     = Segment::segment_manager;

    template <typename T> using Alloc  = bip::allocator<T, Mgr>;
    template <typename T> using Vector = std::vector<T, Alloc<T>>;

    using String = boost::container::basic_string< //
        char, std::char_traits<char>, Alloc<char>>;

    struct Bar {
        using allocator_type = Alloc<char>;
        String first_name, last_name;

        template <typename Alloc> Bar(std::string_view first_name, std::string_view last_name, Alloc alloc) : 
            first_name(first_name, alloc), last_name(last_name, alloc) {}
    };

    struct Snafu {
        std::array<int, 5> no_problem;
    };

    struct Foo {
        Vector<Bar>   bars;
        Vector<Snafu> snafus;

        template <typename Alloc> //
        Foo(Alloc alloc) : bars(alloc)
                         , snafus(alloc) {}
    };
}

#include <iostream>
static inline std::ostream& operator<<(std::ostream& os, Shared::Foo const& foo) {
    os << "Foo\n================\nBars:\n";
    for (auto& [f, l] : foo.bars)
        os << " - " << f << ", " << l << "\n";

    os << "nafus:";
    for (auto& s : foo.snafus) {
        os << "\n - ";
        for (auto el : s.no_problem)
            os << " " << el;
    }
    return os << "\n";
}


int main() { Shared::Segment msm(bip::open_or_create, "my_shared_mem", 10ull << 10);

    Shared::Foo& foo = *msm.find_or_construct<Shared::Foo>("the_foo") //
                        (msm.get_segment_manager()); // constructor arguments

    foo.bars.emplace_back("John", "Doe", msm.get_segment_manager());
    foo.bars.emplace_back("Jane", "Deer", msm.get_segment_manager());
    foo.bars.emplace_back("Igor", "Stravinsky", msm.get_segment_manager());
    foo.bars.emplace_back("Rimsky", "Korsakov", msm.get_segment_manager());

    foo.snafus.push_back({1, 2, 3});
    foo.snafus.push_back({2, 3, 4});
    foo.snafus.push_back({3, 4, 5, 6, 7});

    std::cout << foo << "\n";
}

Output first run:

Foo
================
Bars:
 - John, Doe
 - Jane, Deer
 - Igor, Stravinsky
 - Rimsky, Korsakov
nafus:
 -  1 2 3 0 0
 -  2 3 4 0 0
 -  3 4 5 6 7

Output second run:

Foo
================
Bars:
 - John, Doe
 - Jane, Deer
 - Igor, Stravinsky
 - Rimsky, Korsakov
 - John, Doe
 - Jane, Deer
 - Igor, Stravinsky
 - Rimsky, Korsakov
nafus:
 -  1 2 3 0 0
 -  2 3 4 0 0
 -  3 4 5 6 7
 -  1 2 3 0 0
 -  2 3 4 0 0
 -  3 4 5 6 7

BONUS

As always, I cannot be remiss about scoped_allocator_adaptor, the best thing since sliced semiconductors:

template <typename T>
using Alloc = boost::container::scoped_allocator_adaptor< //
    bip::allocator<T, Mgr>>;

Now, anywhere uses_allocator protocol is used, you get magic allocator propagation:

Live On Coliru

#include <boost/container/scoped_allocator.hpp>
#include <boost/container/string.hpp>
#include <boost/container/vector.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/managed_mapped_file.hpp>
#include <boost/interprocess/managed_shared_memory.hpp>
#include <string>
namespace bip = boost::interprocess;

namespace Shared {
#ifdef COLIRU
    using Segment = bip::managed_mapped_file;
#else
    using Segment = bip::managed_shared_memory;
#endif
    using Mgr     = Segment::segment_manager;

    template <typename T>
    using Alloc = boost::container::scoped_allocator_adaptor< //
        bip::allocator<T, Mgr>>;

    template <typename T> using Vector = std::vector<T, Alloc<T>>;

    using String = boost::container::basic_string< //
        char, std::char_traits<char>, Alloc<char>>;

    struct Bar {
        using allocator_type = Alloc<char>;
        String first_name, last_name;

        template <typename Alloc>
        Bar(std::allocator_arg_t, Alloc alloc) : first_name(alloc)
                                               , last_name(alloc) {}

        template <typename Alloc>
        Bar(Alloc&& alloc) : first_name(alloc)
                           , last_name(alloc) {}

        Bar(Bar const&) = default;
        template <typename Alloc>
        Bar(Bar const& rhs, Alloc alloc) : first_name(rhs.first_name, alloc), last_name(rhs.last_name, alloc) {}

        Bar& operator=(Bar const&) = default;

        template <typename Alloc> Bar(std::string_view first_name, std::string_view last_name, Alloc alloc) : 
            first_name(first_name, alloc), last_name(last_name, alloc) {}
    };

    struct Snafu {
        std::array<int, 5> no_problem;
    };

    struct Foo {
        Vector<Bar>   bars;
        Vector<Snafu> snafus;

        template <typename Alloc> //
        Foo(Alloc alloc) : bars(alloc)
                         , snafus(alloc) {}
    };
}

#include <iostream>
static inline std::ostream& operator<<(std::ostream& os, Shared::Foo const& foo) {
    os << "Foo\n================\nBars:\n";
    for (auto& [f, l] : foo.bars)
        os << " - " << f << ", " << l << "\n";

    os << "Snafus:";
    for (auto& s : foo.snafus) {
        os << "\n - ";
        for (auto el : s.no_problem)
            os << " " << el;
    }
    return os << "\n";
}


int main() { Shared::Segment msm(bip::open_or_create, "my_shared_mem", 10ull << 10);
    Shared::Foo& foo = *msm.find_or_construct<Shared::Foo>("the_foo") //
                        (msm.get_segment_manager()); // constructor arguments

    foo.bars.emplace_back("John", "Doe");
    foo.bars.emplace_back("Jane", "Deer");
    foo.bars.emplace_back("Igor", "Stravinsky");
    foo.bars.emplace_back("Rimsky", "Korsakov");

    foo.snafus.push_back({1, 2, 3});
    foo.snafus.push_back({2, 3, 4});
    foo.snafus.push_back({3, 4, 5, 6, 7});

    std::cout << foo << "\n";
}

With identical output

sehe
  • 374,641
  • 47
  • 450
  • 633
  • > "You're mixing "shared address space" and "address mapped at the same address". Because if there was no sharing of address space, there would also not be an issue storing raw pointers." This part no longer makes sense with the answer moved to this question. – Bruce Adams May 20 '22 at 07:16
  • 1
    @Paul the linked COLIRU shows success. If you repeat with current boost version, you apparently have to explicitly include `vector` and `array`, not a big issue: http://coliru.stacked-crooked.com/a/706ca34770abbb27 – sehe Feb 06 '23 at 23:23
  • @sehe Thanks, I am following a lot of your answers, and I have some problems because you don't like classes a lot, you give answers using procedures or structs, I am scratching my head trying to translate that to objects – Paul Feb 08 '23 at 23:17
  • 1
    @Paul That's funny, because objects are just functions with bound state. So, you normally start by just putting all members into a class, and them perhaps promoting arguments/local variables to members where appropriate. (By the way, I don't dislike classes. It's just that using them where they are not required makes it harder to integrate into existing code that already has classes) – sehe Feb 08 '23 at 23:44
  • 1
    @Paul If you have a specific example in mind, let me know and perhaps I can illustrate. – sehe Feb 08 '23 at 23:44
  • @sehe I've posted this one https://stackoverflow.com/questions/75417879/boost-interprocess-writer-bad-alloc-when-reader-killed – Paul Feb 11 '23 at 03:14
0

The answer according to the documentation is no.

Limitations When Constructing Objects In Mapped Regions

Offset pointers instead of raw pointers References forbidden Virtuality forbidden Be careful with static class members

When two processes create a mapped region of the same mappable object, two processes can communicate writing and reading that memory. A process could construct a C++ object in that memory so that the second process can use it. However, a mapped region shared by multiple processes, can't hold any C++ object, because not every class is ready to be a process-shared object, specially, if the mapped region is mapped in different address in each process.

If you could get it to work with boost.interprocess it probably wouldn't be portable.

Boost does provide an alternative solution. There are several pieces of magic which make this work:

  • offset pointers
  • telling a template type (e.g. container) to use offset pointers instead of raw pointers via the allocator
  • cascading this instruction to templated types automatically

Boost provides containers which use smart offset pointers (boost::interprocess::offset_ptr) which can be used in place of regular STL containers.

https://stackoverflow.com/a/7159729/1569204 has a concise explanation of how this type of pointer can be implemented (not boost specific):

template<class T> class offset_ptr {
    size_t offset; public:
    T* operator->() { return reinterpret_cast<T*>(reinterpret_cast<char*>(this)+offset); } };

This is very clever. I'm not sure why this has not yet made it into the ISO standard (perhaps it has and I am just ignorant).

See https://en.cppreference.com/w/cpp/named_req/Allocator#Fancy_pointers

When the member type pointer is not a raw pointer type, it is commonly referred to as a "fancy pointer". Such pointers were introduced to support segmented memory architectures and are used today to access objects allocated in address spaces that differ from the homogeneous virtual address space that is accessed by raw pointers. An example of a fancy pointer is the mapping address-independent pointer boost::interprocess::offset_ptr, which makes it possible to allocate node-based data structures such as std::set in shared memory and memory mapped files mapped in different addresses in every process. Fancy pointers can be used independently of the allocator that provided them, through the class template std::pointer_traits (since C++11). The function std::to_address can be used to obtain a raw pointer from a fancy pointer. (since C++20)

Use of fancy pointers and customized size/different type in the standard libary are conditionally supported. Implementations may require that member type pointer, const_pointer, size_type, and difference_type are value_type*, const value_type*, std::size_t, and std::ptrdiff_t, respectively.

There is a good explanation here:

The boost version of the containers uses the pointer type specified by the allocator. This is why allocator_traits includes a pointer type. In theory an STL implementation (post C++11) should use it instead of assuming a raw pointer. Boosts containers ensure that this is done. So the boost allocator not only returns offset_ptr pointers but informs the container template that it should use them as well.

The other bit of magic is a standard way of getting a template to use the allocator of the structure containing it. See What is "uses allocator" and "scoped allocator" construction in c++

This bit of magic has been in C++ since at least C++11.

Bruce Adams
  • 4,953
  • 4
  • 48
  • 111
  • Counterpoint: you can but they need to be fancy pointers. I have [multiple dozens (at least) of answers](https://stackoverflow.com/search?tab=newest&q=user%3a85371%20interprocess%20allocator) up on this site showing various degrees of doing this safely. Now I admit, this might not have been what OP was thinking about, but we really can't tell because the question was pretty shallow. – sehe May 19 '22 at 20:37
  • The linked question is deeper. It also has a bounty. Feel free to edit this one if want to make it clearer. – Bruce Adams May 19 '22 at 21:17
  • Looking at some of the answers you suggest. It seems that boost.interprocess can use boost versions of containers to work around the raw pointer issue. Could you point to one canonical answer? I was intending to accept the boost can't do it answer but I may need to rethink that. – Bruce Adams May 19 '22 at 21:33
  • The canonical answer is the documentation. I didn't see a linked question before, so I'll find it in case I can help out there – sehe May 19 '22 at 21:54
  • @sehe Another thing you might be able to help with - https://stackoverflow.com/questions/72311730/what-is-uses-allocator-and-scoped-allocator-construction-in-c . This question has introduced me to the concepts. I think I understand them but I am not confident yet that I could explain them well to someone else. – Bruce Adams May 19 '22 at 22:33