关于Windows:如何计算GetModuleFileName的完整缓冲区大小?

How can I calculate the complete buffer size for GetModuleFileName?

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

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

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

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


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

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

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

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

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

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

MSDN在标题为"文件名,路径和命名空间"的文章中对路径长度进行了说明:

Maximum Path Length

In the Windows API (with some
exceptions discussed in the following
paragraphs), the maximum length for a
path is MAX_PATH, which is defined as
260 characters. A local path is
structured in the following order:
drive letter, colon, backslash,
components separated by backslashes,
and a terminating null character. For
example, the maximum path on drive D
is"D:\" where"" represents
the invisible terminating null
character for the current system
codepage. (The characters < > are used
here for visual clarity and cannot be
part of a valid path string.)

Note File I/O functions in the
Windows API convert"/" to"\" as part
of converting the name to an NT-style
name, except when using the"\\?\"
prefix as detailed in the following
sections.

The Windows API has many functions
that also have Unicode versions to
permit an extended-length path for a
maximum total path length of 32,767
characters. This type of path is
composed of components separated by
backslashes, each up to the value
returned in the
lpMaximumComponentLength parameter of
the GetVolumeInformation function. To
specify an extended-length path, use
the"\\?\" prefix. For example,
"\\?\D:\". (The
characters < > are used here for
visual clarity and cannot be part of a
valid path string.)

Note The maximum path of 32,767
characters is approximate, because the
"\\?\" prefix may be expanded to a
longer string by the system at run
time, and this expansion applies to
the total length.

The"\\?\" prefix can also be used
with paths constructed according to
the universal naming convention (UNC).
To specify such a path using UNC, use
the"\\?\UNC\" prefix. For example,
"\\?\UNC\server\share", where"server"
is the name of the machine and"share"
is the name of the shared folder.
These prefixes are not used as part of
the path itself. They indicate that
the path should be passed to the
system with minimal modification,
which means that you cannot use
forward slashes to represent path
separators, or a period to represent
the current directory. Also, you
cannot use the"\\?\" prefix with a
relative path, therefore relative
paths are limited to MAX_PATH
characters as previously stated for
paths not using the"\\?\" prefix.

When using an API to create a
directory, the specified path cannot
be so long that you cannot append an
8.3 file name (that is, the directory name cannot exceed MAX_PATH minus 12).

The shell and the file system have
different requirements. It is possible
to create a path with the Windows API
that the shell user interface might
not be able to handle.

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

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

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


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


虽然API证明设计不好,但解决方案实际上非常简单。简单但令人遗憾的是,必须这样做,因为这可能会降低性能,因为它可能需要多个内存分配。这是解决方案的一些关键点:

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

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

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

这意味着,如果返回的值恰好等于缓冲区大小,则您仍然不知道它是否成功。可能会有更多数据。或不。最后,只有缓冲区长度实际上大于要求的长度,您才能确定成功。可悲的是...

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

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

实现和用法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/* 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
", path);

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

    return 0;
}

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

尽管这是现有的最差的Win32 API之一,但我仍然希望您有很多编码乐趣。


运用

1
extern char* _pgmptr

可能有用。

从GetModuleFileName的文档中:

The global variable _pgmptr is automatically initialized to the full path of the executable file, and can be used to retrieve the full path name of an executable file.

但是,如果我读到有关_pgmptr的内容:

When a program is not run from the command line, _pgmptr might be initialized to the program name (the file's base name without the file name extension) or to a file name, relative path, or full path.

有谁知道_pgmptr的初始化方式吗?如果SO支持后续问题,我会将此问题发布为后续问题。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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"";
}


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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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应用程序)的情况。但是以防万一上述解决方案存在后备情况。对我来说似乎有点丑陋,但仍然可以完成工作。