【问题标题】:How can I calculate the complete buffer size for GetModuleFileName?如何计算 GetModuleFileName 的完整缓冲区大小?
【发布时间】:2009-04-30 07:45:12
【问题描述】:

GetModuleFileName() 将缓冲区和缓冲区大小作为输入;但是它的返回值只能告诉我们复制了多少个字符,以及大小是否不够(ERROR_INSUFFICIENT_BUFFER)。

如何确定保存GetModuleFileName() 的整个文件名所需的实际缓冲区大小?

大多数人使用MAX_PATH,但我记得路径可以超过那个(默认定义为 260)...

(使用零作为缓冲区大小的技巧不适用于此 API - 我之前已经尝试过)

【问题讨论】:

标签: windows winapi getmodulefilename


【解决方案1】:

通常的方法是调用它,将大小设置为零,保证失败并提供分配足够缓冲区所需的大小。分配一个缓冲区(不要忘记空终止的空间)并再次调用它。

在很多情况下MAX_PATH 就足够了,因为许多文件系统限制了路径名的总长度。但是,可以构造超过MAX_PATH 的合法且有用的文件名,因此查询所需的缓冲区可能是个好建议。

不要忘记最终从提供它的分配器返回缓冲区。

编辑:弗朗西斯在评论中指出,通常的配方不适用于GetModuleFileName()。不幸的是,弗朗西斯在这一点上是绝对正确的,我唯一的借口是在提供“通常”的解决方案之前我没有去查证。

我不知道那个 API 的作者在想什么,除了它可能在引入时,MAX_PATH 确实是最大可能的路径,使得正确的配方变得容易。只需在长度不少于MAX_PATH 个字符的缓冲区中进行所有文件名操作。

哦,是的,不要忘记自 1995 年左右以来的路径名称允许使用 Unicode 字符。因为 Unicode 占用更多空间,所以任何路径名前面都可以有 \\?\ 以明确请求删除该名称的字节长度限制 MAX_PATH。这使问题变得复杂。

MSDN 在标题为 File Names, Paths, and Namespaces 的文章中对路径长度有这样的说法:

最大路径长度

在 Windows API 中(带有一些 以下讨论的例外情况 段落),最大长度为 路径为MAX_PATH,定义为 260 个字符。本地路径是 结构如下: 驱动器号、冒号、反斜杠、 由反斜杠分隔的组件, 和一个终止的空字符。为了 例如,驱动器 D 上的最大路径 是“D:\<some 256 character path string><NUL>”,其中“&lt;NUL&gt;”代表 不可见的终止空 当前系统的字符 代码页。 (使用字符&lt;&gt; 这里是为了视觉清晰,不能 有效路径字符串的一部分。)

注意文件 I/O 函数 Windows API 将“/”转换为“\”作为一部分 将名称转换为 NT 样式 名称,除非使用“\\?\” 前缀如下详述 部分。

Windows API 有很多功能 也有 Unicode 版本 允许一个扩展长度的路径 最大总路径长度为 32,767 人物。这种类型的路径是 由分开的组件组成 反斜杠,每个最大的值 在返回 lpMaximumComponentLength参数 GetVolumeInformation 函数。到 指定一个扩展长度的路径,使用 “\\?\”前缀。例如, “\\?\D:\&lt;very long path&gt;”。 (这 字符 &lt; &gt; 在这里用于 视觉清晰度,不能成为 有效的路径字符串。)

注意最大路径32767 字符是近似的,因为 “\\?\”前缀可以扩展为 系统在运行时更长的字符串 时间,这种扩展适用于 总长度。

也可以使用“\\?\”前缀 根据构建的路径 通用命名约定 (UNC)。 要使用 UNC 指定这样的路径,请使用 “\\?\UNC\”前缀。例如, “\\?\UNC\server\share”,其中“服务器” 是机器的名称和“共享” 是共享文件夹的名称。 这些前缀不用作 路径本身。他们表明 路径应该传递给 系统修改最少, 这意味着你不能使用 正斜杠表示路径 分隔符,或表示的句点 当前目录。还有,你 不能将“\\?\”前缀与 相对路径,因此是相对的 路径仅限于MAX_PATH 如前所述的字符 不使用“\\?\”前缀的路径。

当使用 API 创建一个 目录,指定路径不能 太长以至于你不能附加一个 8.3 文件名(即目录名不能超过MAX_PATH减12)。

shell 和文件系统有 不同的要求。有可能的 使用 Windows API 创建路径 shell 用户界面可能 处理不了。

所以一个简单的答案是分配一个大小为MAX_PATH 的缓冲区,检索名称并检查错误。如果合适,你就完成了。否则,如果它以“\\?\”开头,则获得大小为 64KB 左右的缓冲区(上面的短语“32,767 个字符的最大路径是近似的”在这里有点麻烦,所以我留下一些细节以供进一步研究)和再试一次。

溢出MAX_PATH 但不以“\\?\”开头似乎是“不可能发生”的情况。同样,接下来要做什么是您必须处理的细节。

对于以“\\Server\Share\”开头的网络名称的路径长度限制也可能有些混淆,更不用说内核对象名称空间中以“\\.\”开头的名称了。上面的文章没有说,我不确定这个API是否可以返回这样的路径。

【讨论】:

  • 一些 API(如 MultibyteToWideChar)允许零大小输入,但 GetModuleFileName 不允许 - 它的返回值是“复制了多少字节”。所以大小为零总是返回零。这不起作用。
  • 哎哟。你是对的,我已经编辑了我的答案,以反映我可能会尝试做些什么来解决这个问题。由于路径可以达到“大约 32,000 个字符”并且可能是 Unicode,因此很好地猜测缓冲区大小并非易事。
【解决方案2】:

实施一些合理的策略来增加缓冲区,例如从 MAX_PATH 开始,然后使每个连续的大小比前一个大 1.5 倍(或迭代次数少的 2 倍)。迭代直到函数成功。

【讨论】:

  • 如果缓冲区太小而无法容纳模块名称,则字符串被截断为 nSize 个字符,包括终止空字符,函数返回 nSize,函数将最后一个错误设置为 ERROR_INSUFFICIENT_BUFFER。 Windows XP:如果缓冲区太小而无法容纳模块名称,则函数返回 nSize。最后一个错误代码仍然是 ERROR_SUCCESS。如果 nSize 为零,则返回值为零,最后一个错误代码为 ERROR_SUCCESS。
  • 请注意,在 XP 上,如果发生截断,缓冲区不会以空值终止。但无论哪种方式,如果缓冲区太小,该函数在所有 Windows 版本上都会返回 nSize,因此您通常可以忽略错误代码和当前缓冲区内容,只需增大缓冲区并递增 nSize,然后再试一次,重复直到它停止返回nSize
  • @Remy Lebeau:如果函数由于缓冲区长度不足以外的其他原因而失败怎么办?他最终可能会导致 RAM 量不足。
  • @Paul 如果发生任何错误除了 ERROR_INSUFFICIENT_BUFFER,返回值将是0而不是nSize。仅当返回值为 nSize 时,我才建议使用循环。
【解决方案3】:

虽然 API 可以证明设计不佳,但解决方案实际上非常简单。很简单,但很遗憾,它必须是这样,因为它可能需要多次内存分配,这有点消耗性能。以下是解决方案的一些要点:

  • 您不能真正依赖不同 Windows 版本之间的返回值,因为它在不同 Windows 版本(例如 XP)上可能具有不同的语义。

  • 如果提供的缓冲区太小而无法容纳字符串,则返回值是包含 0 终止符的字符数。

  • 如果提供的缓冲区足够大以容纳字符串,则返回值是不包括 0 终止符的字符数。

这意味着如果返回值正好等于缓冲区大小,你仍然不知道它是否成功。可能还有更多数据。或不。最后,只有当缓冲区长度实际上大于要求时,您才能确定成功。可惜……

因此,解决方案是从一个小缓冲区开始。然后我们调用 GetModuleFileName 传递确切的缓冲区长度(以 TCHAR 为单位)并将返回结果与它进行比较。如果返回结果小于我们的缓冲区长度,则表示成功。如果返回结果大于或等于我们的缓冲区长度,我们必须用更大的缓冲区再试一次。冲洗并重复直到完成。完成后,我们制作缓冲区的字符串副本 (strdup/wcsdup/tcsdup),清理并返回字符串副本。该字符串将具有正确的分配大小,而不是我们临时缓冲区的可能开销。请注意,调用者负责释放返回的字符串(strdup/wcsdup/tcsdup mallocs 内存)。

请参阅下面的实现和使用代码示例。我已经使用这个代码十多年了,包括在企业文档管理软件中可能有很多很长的路径。代码当然可以通过各种方式进行优化,例如首先将返回的字符串加载到本地缓冲区(TCHAR buf[256])。如果该缓冲区太小,您可以启动动态分配循环。其他优化也是可能的,但这超出了这里的范围。

实现和使用示例:

/* Ensure Win32 API Unicode setting is in sync with CRT Unicode setting */
#if defined(_UNICODE) && !defined(UNICODE)
#   define UNICODE
#elif defined(UNICODE) && !defined(_UNICODE)
#   define _UNICODE
#endif

#include <stdio.h> /* not needed for our function, just for printf */
#include <tchar.h>
#include <windows.h>

LPCTSTR GetMainModulePath(void)
{
    TCHAR* buf    = NULL;
    DWORD  bufLen = 256;
    DWORD  retLen;

    while (32768 >= bufLen)
    {
        if (!(buf = (TCHAR*)malloc(sizeof(TCHAR) * (size_t)bufLen))
        {
            /* Insufficient memory */
            return NULL;
        }

        if (!(retLen = GetModuleFileName(NULL, buf, bufLen)))
        {
            /* GetModuleFileName failed */
            free(buf);
            return NULL;
        }
        else if (bufLen > retLen)
        {
            /* Success */
            LPCTSTR result = _tcsdup(buf); /* Caller should free returned pointer */
            free(buf);
            return result;
        }

        free(buf);
        bufLen <<= 1;
    }

    /* Path too long */
    return NULL;
}

int main(int argc, char* argv[])
{
    LPCTSTR path;

    if (!(path = GetMainModulePath()))
    {
        /* Insufficient memory or path too long */
        return 0;
    }

    _tprintf("%s\n", path);

    free(path); /* GetMainModulePath malloced memory using _tcsdup */ 

    return 0;
}

说了这么多,我想指出您需要非常了解 GetModuleFileName(Ex) 的其他各种注意事项。 32/64 位/WOW64 之间存在不同的问题。此外,输出不一定是完整的长路径,但很可能是短文件名或受路径别名的影响。我希望当您使用这样一个函数时,目标是为调用者提供一个可用、可靠的完整、长路径,因此我建议确实确保返回一个可用、可靠、完整、长的绝对路径,以这样的方式它可以在各种 Windows 版本和体系结构之间移植(同样是 32/64 位/WOW64)。如何有效地做到这一点超出了这里的范围。

虽然这是现有的最糟糕的 Win32 API 之一,但我还是希望您能享受编码乐趣。

【讨论】:

    【解决方案4】:

    我的示例是“如果一开始没有成功,则将缓冲区的长度加倍”方法的具体实现。它使用字符串(实际上是wstring,因为我希望能够处理Unicode)作为缓冲区来检索正在运行的可执行文件的路径。为了确定何时成功检索到完整路径,它会检查从GetModuleFileNameW 返回的值与wstring::length() 返回的值,然后使用该值调整最终字符串的大小以去除多余的空字符。如果失败,则返回一个空字符串。

    inline std::wstring getPathToExecutableW() 
    {
        static const size_t INITIAL_BUFFER_SIZE = MAX_PATH;
        static const size_t MAX_ITERATIONS = 7;
        std::wstring ret;
        DWORD bufferSize = INITIAL_BUFFER_SIZE;
        for (size_t iterations = 0; iterations < MAX_ITERATIONS; ++iterations)
        {
            ret.resize(bufferSize);
            DWORD charsReturned = GetModuleFileNameW(NULL, &ret[0], bufferSize);
            if (charsReturned < ret.length())
            {
                ret.resize(charsReturned);
                return ret;
            }
            else
            {
                bufferSize *= 2;
            }
        }
        return L"";
    }
    

    【讨论】:

      【解决方案5】:

      使用

      extern char* _pgmptr
      

      可能会起作用。

      来自 GetModuleFileName 的文档:

      全局变量_pgmptr自动初始化为可执行文件的全路径,可用于检索可执行文件的全路径名。

      但如果我读到 _pgmptr:

      当程序不是从命令行运行时,_pgmptr 可能会被初始化为程序名(不带文件扩展名的文件的基本名称)或文件名、相对路径或完整路径。

      谁知道 _pgmptr 是如何初始化的?如果 SO 支持后续问题,我会将这个问题作为后续问题发布。

      【讨论】:

        【解决方案6】:

        这是另一个使用 std::wstring 的解决方案:

        DWORD getCurrentProcessBinaryFile(std::wstring& outPath)
        {
            // @see https://msdn.microsoft.com/en-us/magazine/mt238407.aspx
            DWORD dwError  = 0;
            DWORD dwResult = 0;
            DWORD dwSize   = MAX_PATH;
        
            SetLastError(0);
            while (dwSize <= 32768) {
                outPath.resize(dwSize);
        
                dwResult = GetModuleFileName(0, &outPath[0], dwSize);
                dwError  = GetLastError();
        
                /* if function has failed there is nothing we can do */
                if (0 == dwResult) {
                    return dwError;
                }
        
                /* check if buffer was too small and string was truncated */
                if (ERROR_INSUFFICIENT_BUFFER == dwError) {
                    dwSize *= 2;
                    dwError = 0;
        
                    continue;
                }
        
                /* finally we received the result string */
                outPath.resize(dwResult);
        
                return 0;
            }
        
            return ERROR_BUFFER_OVERFLOW;
        }
        

        【讨论】:

        • 请注意,如果GetModuleFileName() 成功(当返回值为&gt; 0&lt; dwSize 时)调用GetLastError()未定义的行为。仅当GetModuleFileName() 失败(返回值为0 或dwSize 时)返回有效 错误代码。此外,此代码应显式调用GetModuleFileNameW(),而不是依赖项目配置使GetModuleFileName() 在预处理器阶段映射到GetModuleFileNameW()
        【解决方案7】:

        Windows 无法正确处理超过 260 个字符的路径,因此只需使用 MAX_PATH。 您不能运行路径长于 MAX_PATH 的程序。

        【讨论】:

        【解决方案8】:

        我的方法是使用 argv,假设您只想获取正在运行的程序的文件名。当您尝试从不同的模块获取文件名时,已经描述了没有任何其他技巧的唯一安全方法,可以在此处找到实现。

        // assume argv is there and a char** array
        
        int        nAllocCharCount = 1024;
        int        nBufSize = argv[0][0] ? strlen((char *) argv[0]) : nAllocCharCount;
        TCHAR *    pszCompleteFilePath = new TCHAR[nBufSize+1];
        
        nBufSize = GetModuleFileName(NULL, (TCHAR*)pszCompleteFilePath, nBufSize);
        if (!argv[0][0])
        {
            // resize memory until enough is available
            while (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
            {
                delete[] pszCompleteFilePath;
                nBufSize += nAllocCharCount;
                pszCompleteFilePath = new TCHAR[nBufSize+1];
                nBufSize = GetModuleFileName(NULL, (TCHAR*)pszCompleteFilePath, nBufSize);
            }
        
            TCHAR * pTmp = pszCompleteFilePath;
            pszCompleteFilePath = new TCHAR[nBufSize+1];
            memcpy_s((void*)pszCompleteFilePath, nBufSize*sizeof(TCHAR), pTmp, nBufSize*sizeof(TCHAR));
        
            delete[] pTmp;
            pTmp = NULL;
        }
        pszCompleteFilePath[nBufSize] = '\0';
        
        // do work here
        // variable 'pszCompleteFilePath' contains always the complete path now
        
        // cleanup
        delete[] pszCompleteFilePath;
        pszCompleteFilePath = NULL;
        

        我还没有遇到 argv 不包含文件路径(Win32 和 Win32-console 应用程序)的情况。但以防万一出现上述解决方案的回退。对我来说似乎有点难看,但仍然可以完成工作。

        【讨论】:

        • 这段代码有很多问题,包括破坏堆栈。如果您的解决方案包含“您可能会收到崩溃报告”,那么您可能不应该发布它。
        • 我发布了它,所以也许有人可以找到解决问题的解决方案。这只是第一个想法。
        • 我认为您甚至不了解您的代码的作用。不是传入真正的缓冲区,而是传入堆栈中的任意指针。如果你愿意浪费你的筹码(提示:你不是),那么为什么还要麻烦第二次调用它呢?您已经在第一次通话时获得了数据。
        • 您的解决方案都没有提供任何尚未在此线程中讨论过的内容,同样,没有理由调用 GetModuleFileName 两次......老实说,我认为您应该放弃它。
        • 那是因为 argv 不能解决任何问题。恐怕其他人讨论的解决方案比你的要好。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-04-05
        • 2019-05-31
        • 2010-10-21
        • 1970-01-01
        • 2015-11-01
        相关资源
        最近更新 更多