C++ WINAPI 共享内存结构体数组

3
我正尝试使用WINAPI通过命名共享内存共享结构体数组。我能够创建和管理共享内存,但是当尝试共享结构体数组时,读取的数组大小总是为0。
以下是我编写的测试代码,应该可以写入/读取10个条目的数组,但即使这也失败了。然而,我的目标是编写/读取一个包含2个动态数组及其当前信息的动态结构体数组。
我知道不应该在进程之间共享指针,因为它们可能指向随机值。因此,我正在使用new为数组分配内存。
这是我现在拥有的: 两个进程都共享的内容:
#define MEMSIZE 90024 

typedef struct {
    int id;
    int type;
    int count;
} Entry;

步骤 1:

extern HANDLE hMapObject;
extern void* vMapData;

std::vector<Entry> entries;//collection of entries

BOOL DumpEntries(TCHAR* memName) {//Returns true, writing 10 entries
    int size = min(10, entries.size());

    Entry* eArray = new Entry[size];
    for (int i = 0; i < size; i++) {
        eArray[i] = entries.at(i);
    }

    ::hMapObject = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, MEMSIZE, memName);
    if (::hMapObject == NULL) {
        return FALSE;
    }

    ::vMapData = MapViewOfFile(::hMapObject, FILE_MAP_ALL_ACCESS, 0, 0, MEMSIZE);
    if (::vMapData == NULL) {
        CloseHandle(::hMapObject);
        return FALSE;
    }

    CopyMemory(::vMapData, eArray, (size * sizeof(Entry)));
    UnmapViewOfFile(::vMapData);
    //delete[] eArray;
    return TRUE;
}

第二步骤:

BOOL ReadEntries(TCHAR* memName, Entry* entries) {//Returns true reading 0 entries
    HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, memName);
    if (hMapFile == NULL) {
        return FALSE;
    }

    Entry* tmpEntries = (Entry*)(MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 10 * sizeof(Entry)));
    if (tmpEntries == NULL) {
        CloseHandle(hMapFile);
        return FALSE;
    }

    entries = new Entry[10];

    for (int i = 0; i < 10; i++) {
        entries[i] = tmpEntries[i];
    }

    UnmapViewOfFile(tmpEntries);
    CloseHandle(hMapFile);
    return TRUE;
}

写入10个条目似乎是有效的,但是尝试读取内存时,它会成功返回并且数组大小为0,如下所示:
Entry* entries = NULL;
if (ReadEntries(TEXT("Global\Entries"), entries)) {
        int size = _ARRAYSIZE(entries);
        out = "Succesfully read: " + to_string(size);// Is always 0
}

所以我的问题是,我做错了什么?我在两个进程之间共享相同的结构体, 为要写入的条目分配新的内存并使用大小为10 * sizeof(Entry);的内存进行复制。尝试读取时,我也尝试读取10 * sizeof(Entry);字节,并将数据转换为Entry*。我是否漏掉了什么?欢迎任何帮助。


你是否将数组长度和元素存储在共享内存中? - Ignacio Vazquez-Abrams
标记为 .net 或其他,它不是标准的 C++,而是微软的 C++。 - Top Sekret
或者只需使用Posix套接字API。可能有效。 - Top Sekret
@JakubKaszycki:这不是 .net,只是 Winapi... - Serge Ballesta
_ARRAYSIZE(entries) 将扩展为 sizeof(entries) / sizeof( *(entries) ),而且 sizeof(entries) == 0(对于您的情况),这就是为什么 _ARRAYSIZE(entries) == 0。您只能获取固定大小数组的大小。 - Logman
3个回答

2
基于初步检查,这段代码似乎试图将包含std :: string的结构映射到共享内存中,以供另一个进程使用。
不幸的是,在开始之前,这次冒险就注定失败了。即使您成功地传递了数组长度,我也预计另一个进程会立即崩溃,一旦它甚至闻到另一个进程试图映射到共享内存段中的std :: string。
std :: string是非平凡类。std :: string维护指向实际字符串数据所在缓冲区的内部指针;缓冲区在堆上分配。
您明白sizeof(std :: string)不会改变,无论字符串包含五个字符还是“战争与和平”的全部内容,对吧?暂停一下,想一想,如何在只需要几个字节来存储std :: string的情况下做到这一点?
一旦您思考了一会儿,应该很清楚为什么将一个进程的std :: string映射到共享内存段中,然后尝试通过另一个进程来获取它们是行不通的。
唯一可以实际映射到/从共享内存的是纯粹的数据; 虽然在某些情况下你也可以使用聚合。

嘿,感谢您的评论。我并不是真的想分享std::string,因为我正在使用TCHAR*来实现这个目的,我只是将它添加到OP中作为示例。我最担心的是共享命名内存,以实际能够共享包含POINTstd::vector或数组。或者更好的是,能够从一个进程向另一个进程发送一组点(x,y)。一个整数的1D数组也很好。 - MircoProgram
更新了问题。 - MircoProgram

2
抱歉,问题只存在于“_ARRAYSIZE”宏中。我在MSDN上找不到它的确切定义,但我在其他页面上找到了“_countof”或“ARRAYSIZE”的参考。它们都被定义为“sizeof(array)/sizeof(array[0])”。问题在于,这只对真正的数组有意义,如“Entry entries [10]”,而不适用于指向这样一个数组的指针。从技术上讲,当你声明:
Entry* entries;

sizeof(entries)表示sizeof(Entry *),即指针的大小。它比结构体的大小小,所以整数除法的结果是... 0!

无论如何,当前代码中还存在其他问题。通过共享内存交换变量大小的数组的正确方法是使用一个包含大小和数组本身的辅助结构,该结构声明为不完整:

struct EntryArray {
    size_t size;
    Entry entries[];
};

你可以这样倾倒它:
BOOL DumpEntries(TCHAR* memName) {//Returns true, writing 10 entries
    int size = min(10, entries.size());

    EntryArray* eArray = (EntryArray *) malloc(sizeof(EntryArray) + size * sizeof(Entry));
    for (int i = 0; i < size; i++) {
        eArray->entries[i] = entries.at(i);
    }
    eArray->size = size;

    ::hMapObject = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, MEMSIZE, memName);
    if (::hMapObject == NULL) {
        return FALSE;
    }

    ::vMapData = MapViewOfFile(::hMapObject, FILE_MAP_ALL_ACCESS, 0, 0, MEMSIZE);
    if (::vMapData == NULL) {
        CloseHandle(::hMapObject);
        return FALSE;
    }

    CopyMemory(::vMapData, eArray, (sizeof(EntryArray) + size * sizeof(Entry)));
    UnmapViewOfFile(::vMapData);
    free(eArray);
    return TRUE;
}

您会注意到结构体的最后一个成员是不完整数组,因此它分配了0大小,因此您必须分配结构体的大小+数组的大小。

然后您可以通过以下方式从内存中读取它:

size_t ReadEntries(TCHAR* memName, Entry*& entries) {//Returns the number of entries or -1 if error
    HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, memName);
    if (hMapFile == NULL) {
        return -1;
    }

    EntryArray* eArray = (EntryArray*)(MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 10 * sizeof(Entry)));
    if (eArray == NULL) {
        CloseHandle(hMapFile);
        return -1;
    }

    entries = new Entry[10]; // or even entries = new Entry[eArray->size];

    for (int i = 0; i < 10; i++) { // same: i<eArray->size ...
        entries[i] = eArray->entries[i];
    }

    UnmapViewOfFile(eArray);
    CloseHandle(hMapFile);
    return eArray.size;
}

但是在这里你需要注意一些差异。由于eArray消失时条目数也会丢失,因此它作为函数的返回值传递。如果你想要修改作为第二个参数传递的指针,你必须通过引用来传递它(如果你通过值传递它,你只会改变一个本地副本,在函数返回后原始变量仍然为NULL)。

你的代码仍然有一些可能的改进,因为向量entries是全局的,当它可以作为参数传递给DumpEntries时,hMapObject也是全局的,当它可以被函数返回时。在DumpObject中,你可以通过直接在共享内存中构建EntryArray来避免复制:

HANDLE DumpEntries(TCHAR* memName, const std::vector<Entry>& entries) {
    //Returns HANDLE to mapped file (or NULL), writing 10 entries
    int size = min(10, entries.size());

    HANDLE hMapObject = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, MEMSIZE, memName);
    if (hMapObject == NULL) {
        return NULL;
    }

    void * vMapData = MapViewOfFile(hMapObject, FILE_MAP_ALL_ACCESS, 0, 0, MEMSIZE);
    if (vMapData == NULL) {
        CloseHandle(hMapObject);
        return NULL;
    }

    EntryArray* eArray = (EntryArray*) vMapData;
    for (int i = 0; i < size; i++) {
        eArray->entries[i] = entries.at(i);
    }
    eArray->size = size;

    UnmapViewOfFile(vMapData);
    return hMapObject;
}

最后但并非最不重要的是,反斜杠\是字符串文字中的特殊引用字符,必须对它本身进行引用。因此,您应该写成:TEXT("Global\\Entries")


谢谢您的帮助,Entry 结构体内的动态数组应该如何处理?我很快就会测试它。 - MircoProgram

1
我对你的代码进行了一些更改:

PROCESS 1:

BOOL DumpEntries(TCHAR* memName)
{
     int size = entries.size() * sizeof(Entry) + sizeof(DWORD);

     ::hMapObject = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, size, memName);
     if (::hMapObject == NULL) {
          return FALSE;
     }

     ::vMapData = MapViewOfFile(::hMapObject, FILE_MAP_ALL_ACCESS, 0, 0, size);
     if (::vMapData == NULL) {
          CloseHandle(::hMapObject);
          return FALSE;
     }

     (*(DWORD*)::vMapData) = entries.size();
     Entry* eArray = (Entry*)(((DWORD*)::vMapData) + 1);
     for(int i = entries.size() - 1; i >= 0; i--) eArray[i] = entries.at(i);

     UnmapViewOfFile(::vMapData);
     return TRUE;
}

进程2:

BOOL ReadEntries(TCHAR* memName, Entry** entries, DWORD &number_of_entries) {
     HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, memName);
     if (hMapFile == NULL) {
          return FALSE;
     }

     DWORD *num_entries = (DWORD*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 0);
     if (num_entries == NULL) {
          CloseHandle(hMapFile);
          return FALSE;
     }
     number_of_entries = *num_entries;

     if(number_of_entries == 0)
     {
         // special case: when no entries was found in buffer
         *entries = NULL;
         return true;
     }

     Entry* tmpEntries = (Entry*)(num_entries + 1);

     *entries = new Entry[*num_entries];

     for (UINT i = 0; i < *num_entries; i++) {
          (*entries)[i] = tmpEntries[i];
     }

     UnmapViewOfFile(num_entries);
     CloseHandle(hMapFile);

     return TRUE;
}

进程 2(使用示例):

void main()
{
    Entry* entries;
    DWORD number_of_entries;

    if(ReadEntries(TEXT("Global\\Entries", &entries, number_of_entries) && number_of_entries > 0)
    {
        // do something
    }
    delete entries;
}

改动:

  • 我在映射内存时没有使用静态大小(MEMSIZE),而是计算出确切需要的内存
  • 我给内存映射加了一个“头”,即一个DWORD,用于向进程2发送缓冲区中条目的数量
  • 你的ReadEntries定义有误,我将Entry*更改为Entry**进行修复

注意事项:

  • 在进程2调用ReadEntries之前,你需要关闭进程1中的::hMapObject句柄
  • 在使用返回的ReadEntries内存之前,你需要删除进程2返回的条目内存
  • 此代码仅适用于同一Windows用户,如果你想要与用户进程(例如)通信服务,则需要处理CreateFileMapping过程中的SECURITY_ATTRIBUTES成员

这看起来很有前途,稍后会尝试并告诉您。 - MircoProgram
TEXT("Global\Entries") 可以与所有进程一起使用,TEXT("Local\Entries") 只能与在相同用户名空间中的进程一起使用。更详细的解释请参见此处:https://msdn.microsoft.com/en-us/library/windows/desktop/aa382954(v=vs.85).aspx - Ing. Gerardo Sánchez
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - MircoProgram
那是另一个(更简单)的问题,我认为。请在其他问题中提出它。 - Ing. Gerardo Sánchez
我创建了一个新问题:https://stackoverflow.com/questions/38128514/c-winapi-shared-memory-dynamic-arrays - MircoProgram
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接