7

I am a bit confused by the heap and by-value-versus-by-reference semantics involved in putting a std::string key and a large struct value into a container like boost::interprocess::map.

Here is my situation, and some typedefs I'm using:

typedef std::string     AreaKeyType;     
typedef DATA_AREA_DESC          AreaMappedType; // DATA_AREA_DESC is a big struct.
typedef std::pair<const AreaKeyType, AreaMappedType> AreaValueType;
typedef boost::interprocess::allocator<AreaValueType, boost::interprocess::managed_shared_memory::segment_manager> AreaShmemAllocator;
typedef boost::interprocess::map<AreaKeyType, AreaMappedType, std::less<AreaKeyType>, AreaShmemAllocator> AreaMap;

Here is how I'm inserting AreaValueType (which is a typedef for std::pair):

 AreaValueType A(areaKey, arearec);
 anAreaMap->insert(A); 

I believe the above code copies A which is an std::pair on my local (non shared memory) stack into a shared memory area. Can I get a handle to that shared memory area inside the boost::interprocess::map or am I limited to fetching that record back whole and storing it whole? (In other words, can I store something like a structure into a boost interprocess map and then update a single byte inside that record, or do I have to only update the entire record by replacing all the bytes in a DATA_AREA_DESC struct, with entirely new bytes.)

Some further clarification:

  1. I have a plain old ANSI C DLL export api that internally uses C++ and Boost::interprocess::map. The function is expected to create an item in the map and then return a handle. How can I insert something into the boost::interprocess::map and then return a handle to that entity, to non-C++ users, preferably cast to void* or unsigned long? All I can seem to do is fetch stuff from shared memory by looking up the std::string key value, and write a new record into memory. I'd like to instead be able to keep a reference to the shared memory object around.

  2. If I can't directly do that, how would I do it indirectly? I suppose I could keep a non-shared-memory std::vector, and allocate a non-shared memory std::string holding the value of the areaKey, which is a std::string, and then do a cast of the void* item back to std::string and then use that to fetch a record out of the shared memory area. That all seems like more work than should be strictly necessary for something so elementary. Maybe boost::interprocess::map isn't the right choice for my requirements?

What have I tried? This, which compiles, but I have no idea if I am doing this right. Somehow I feel ugly inside dereferencing an ::iterator returned from find, and then immediately taking its address like so:

void ** handle; // actually a parameter in my api.
*handle = (void*)&(*anAreaMap->find(areaKey));

Update The above works. The very sensible advice in the answer below does NOT work however. Using boost::interprocess::string results in complete and total failure and crashes at runtime. Using std::string, which has no right to work unless the authors of Boost coded std::string support in especially, actually works great.

Warren P
  • 65,725
  • 40
  • 181
  • 316

1 Answers1

1

If handle is supposed to be a pointer to the std::pair in shared memory then your code will work provided you know that areaKey is in the map. There's nothing wrong with it except you don't need the explicit cast (and if you do cast then static_cast<void*>() would be preferred).

I haven't used boost::interprocess but I think you will need to use boost::interprocess::string or a std::basic_string with a non-default allocator for your key. Unless boost::interprocess does something fancy under the hood, using std::string will put a pointer to local memory (for the string buffer) into shared memory which won't be meaningful in another process.

Here's a test program that uses a map with string keys:

#include <iostream>
#include <string>
#include <boost/foreach.hpp>
#include <boost/format.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/containers/map.hpp>
#include <boost/interprocess/containers/string.hpp>
#include <boost/interprocess/managed_shared_memory.hpp>

namespace bi = boost::interprocess;

#define SHARED_STRING 1 // set to 1 for interprocess::string, 0 for std::string
static const char *SHARED_MEMORY_NAME = "MySharedMemory";
static const char *SHARED_MAP_NAME = "MySharedMap";

int main(int argc, char *argv[]) {
#if SHARED_STRING
   typedef bi::allocator<char, bi::managed_shared_memory::segment_manager> CharAllocator;
   typedef bi::basic_string<char, std::char_traits<char>, CharAllocator> Key;
#else
   typedef std::allocator<char> CharAllocator;
   typedef std::basic_string<char, std::char_traits<char>, CharAllocator> Key;
#endif

   typedef int Mapped;
   typedef std::pair<const Key, Mapped> Value;
   typedef bi::allocator<Value, bi::managed_shared_memory::segment_manager> MapAllocator;
   typedef bi::map<Key, Mapped, std::less<Key>, MapAllocator> Map;

   bi::managed_shared_memory *segment;
   Map *map;
   if (argc <= 1) {
      // Create new shared memory segment.
      bi::shared_memory_object::remove(SHARED_MEMORY_NAME);
      segment = new bi::managed_shared_memory(bi::create_only, SHARED_MEMORY_NAME, 65536);

      MapAllocator mapAllocator(segment->get_segment_manager());
      map = segment->construct<Map>(SHARED_MAP_NAME)(std::less<Key>(), mapAllocator);
      assert(map);
   }
   else {
      // Open existing shared memory segment.
      segment = new bi::managed_shared_memory(bi::open_only, SHARED_MEMORY_NAME);

      map = segment->find<Map>(SHARED_MAP_NAME).first;
      assert(map);
   }

#if SHARED_STRING
   CharAllocator charAllocator(segment->get_segment_manager());
#else
   CharAllocator charAllocator;
#endif
   while (true) {
      std::string input;
      if (!getline(std::cin, input))
         break;

      map->insert(std::make_pair(Key(input.begin(), input.end(), charAllocator), 0));

      BOOST_FOREACH(const Value& value, *map)
         std::cout << boost::format("('%s',%d)\n") % value.first % value.second;
   }

   delete segment;
   bi::shared_memory_object::remove(SHARED_MEMORY_NAME);

   return 0;
}

Run it with no arguments to create a new shared memory segment and with at least one argument to open an existing shared memory segment (a no-argument invocation must already be running). In both cases, the program will iteratively read a key from stdin, insert an entry into the map, and write contents to stdout.

rhashimoto
  • 15,650
  • 2
  • 52
  • 80
  • I don't need to share the untyped values across processes, I simply need to provide an untyped API inside a single client or server, and will internally use the Boost C++ APIs, which will be flattened down to Plain Old C for DLL interface ABI (`Application binary interface`) purposes. – Warren P Apr 01 '13 at 14:40
  • I understand that you just need a plain old `void*` for your handle. What I was trying to say in my second paragraph is that your internal map won't work correctly across the processes that share it unless its string key is valid across processes, and it won't be valid if it is a `std::string`. That is, if you have two processes trying to look up values in the map, or one process inserting values and another reading values, this probably isn't going to work with an ordinary `std::string` as the map key. If you don't need this capability then I guess I don't understand why the map is shared. – rhashimoto Apr 01 '13 at 15:15
  • So the boost MAP type (`boost::interprocess::map`) should be using `boost::interprocess::map` right? I appreciate the tip as I had not thought of it. – Warren P Apr 01 '13 at 15:52
  • Yes, that appears to be the safest thing. That's what I was trying to get across. You can think of the key string as itself a shared container. – rhashimoto Apr 01 '13 at 16:10
  • While I would love to mark your answer correct, the behaviour I have observed while using Std::String and Boost::Interprocess::String is the exact opposite of what I would have expected. Using Boost::InterProcess::String as a Key type in the map results in crashes, access violations and corrupt data in simple tests, whereas using std::string works, and that points to some MAGIC going on in the boost interprocess map implementation. – Warren P Apr 06 '13 at 17:22
  • That's strange. I see exactly the opposite - `boost::interprocess::string works from separate processes and `std::string` does not. I've added my test program to the answer. You can modify the `SHARED_STRING` define to change the string implementation. I want to verify two things - (1) that you are testing insert and lookup from different processes, and (2) that you are constructing shared memory strings with an allocator for your shared memory segment. – rhashimoto Apr 06 '13 at 21:15
  • Yes I am inserting from process #1 and reading back from process #2. I am totally surprised. I will try to post a minimal non-working example. – Warren P Apr 06 '13 at 22:28
  • I have a possible explanation. Since you mention DLLs, I'm guessing that you're using Visual C++, and according to [this](http://stackoverflow.com/questions/5419016/string-class-allocating-on-stack-for-small-strings) at least some versions of VC++ implement `std::string` with the [short string optimization](http://stackoverflow.com/questions/10315041/meaning-of-acronym-sso-in-the-context-of-stdstring). If that is the case and your key strings are short enough then their storage will not be dynamically allocated. You can test this by trying keys that are longer than `sizeof(std::string)`. – rhashimoto Apr 07 '13 at 00:11