windows 如何计算GetModuleFileName的完整缓冲区大小?

sf6xfgos  于 2023-06-24  发布在  Windows
关注(0)|答案(9)|浏览(194)

GetModuleFileName()将缓冲区和缓冲区大小作为输入;然而,它的返回值只能告诉我们复制了多少个字符,以及大小是否不够(ERROR_INSUFFICIENT_BUFFER)。
如何确定真实的需要的缓冲区大小来保存GetModuleFileName()的整个文件名?
大多数人使用MAX_PATH,但我记得路径可以超过这个值(默认定义为260)。
(The使用零作为缓冲区大小的技巧不适用于此API -我以前已经尝试过)

pw136qt2

pw136qt21#

通常的方法是调用它,将大小设置为零,保证失败,并提供分配足够缓冲区所需的大小。分配一个缓冲区(不要忘记nul-termination的空间)并再次调用它。
在许多情况下,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>”,其中“<NUL>”表示当前系统代码页的不可见终止空字符。(此处使用字符<>是为了视觉清晰,不能作为有效路径字符串的一部分。)
注意Windows API中的文件I/O函数将“/”转换为“\”,作为将名称转换为NT样式名称的一部分,除非使用“\\?\”前缀,如以下各节所述。
Windows API有许多函数,这些函数也具有Unicode版本,以允许最大总路径长度为32,767个字符的扩展长度路径。这种类型的路径由反斜杠分隔的组件组成,每个组件的最大值为GetVolumeInformation函数的lpMaximumComponentLength参数中返回的值。要指定扩展长度路径,请使用“\\?\”前缀。例如,“\\?\D:\<very long path>”。(此处使用字符<>是为了视觉清晰,不能作为有效路径字符串的一部分。)
注意:32,767个字符的最大路径是近似值,因为“\\?\”前缀可能会在运行时被系统扩展为更长的字符串,并且此扩展适用于总长度。
\\?\”前缀也可以用于根据通用命名约定(UNC)构造的路径。要使用UNC指定这样的路径,请使用“\\?\UNC\”前缀。例如,“\\?\UNC\server\share”,其中“server”是计算机的名称,“share”是共享文件夹的名称。这些前缀不用作路径本身的一部分。它们表明路径应该以最小的修改传递给系统,这意味着不能使用正斜杠表示路径分隔符,也不能使用句点表示当前目录。此外,您不能将“\\?\”前缀与相对路径一起使用,因此相对路径仅限于MAX_PATH字符,如前面针对不使用“\\?\”前缀的路径所述。
使用API创建目录时,指定的路径不能太长,不能追加8.3文件名(即目录名不能超过MAX_PATH减12)。
shell和文件系统有不同的要求。可以使用Windows API创建shell用户界面可能无法处理的路径。
因此,一个简单的答案是分配一个大小为MAX_PATH的缓冲区,检索名称并检查错误。如果合适,你就完成了。否则,如果它以“\\?\”开头,则获取一个大小为64 KB左右的缓冲区(上面的短语“32,767个字符的最大路径是近似的”在这里有点麻烦,所以我留下一些细节供进一步研究)并重试。
溢出MAX_PATH但不以“\\?\”开头似乎是“不可能发生”的情况。再一次,接下来要做什么是你必须处理的细节。
对于以“\\Server\Share\”开头的网络名称的路径长度限制,也可能存在一些混淆,更不用说以“\\.\”开头的内核对象名称空间中的名称了。上面的文章没有说,我不确定这个API是否可以返回这样的路径。

cgvd09ve

cgvd09ve2#

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

o8x7eapl

o8x7eapl3#

使用

extern char* _pgmptr

可能会有用
从GetModuleFileName的文档中:
全局变量_pgmptr会自动初始化为可执行文件的完整路径,并可用于检索可执行文件的完整路径名。
但是如果我读到关于_pgmptr:
当程序不是从命令行运行时,_pgmptr可能初始化为程序名(文件的基本名,不带文件扩展名)或文件名、相对路径或完整路径。
谁知道_pgmptr是如何初始化的?如果SO支持后续问题,我会把这个问题作为后续问题。

2wnc66cl

2wnc66cl4#

虽然API证明了糟糕的设计,但解决方案实际上非常简单。很简单,但不幸的是,它必须这样做,因为它有点像性能Pig,因为它可能需要多个内存分配。以下是解决方案的一些关键点:

  • 您不能真正依赖不同Windows版本之间的返回值,因为它在不同的Windows版本上可能有不同的语义(例如XP)。
  • 如果提供的缓冲区太小,无法容纳字符串,则返回值为包括0结束符在内的字符数。
  • 如果提供的缓冲区足够大,可以容纳字符串,则返回值为不包括0结束符的字符数。

这意味着如果返回值正好等于缓冲区大小,您仍然不知道它是否成功。可能会有更多的数据。最后,只有当缓冲区长度实际上大于所需长度时,才能确定成功。很遗憾...
因此,解决方案是从一个小缓冲区开始。然后调用GetModuleFileName,传递精确的缓冲区长度(以TCHAR为单位),并将返回结果与之进行比较。如果返回的结果小于我们的缓冲区长度,则它成功了。如果返回结果大于或等于缓冲区长度,我们必须使用更大的缓冲区再试一次。冲洗并重复直到完成。完成后,我们创建缓冲区的字符串副本(strdup/wcsdup/tcsdup),清理并返回字符串副本。这个字符串将具有正确的分配大小,而不是临时缓冲区的可能开销。请注意,调用者负责释放返回的字符串(strdup/wcsdup/tcsdup错误内存)。
参见下面的实现和用法代码示例。我已经使用这段代码十多年了,包括在企业文档管理软件中,那里可能有很多相当长的路径。当然,代码可以通过各种方式进行优化,例如首先将返回的字符串加载到本地缓冲区(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位/WOW 64之间存在各种问题。同样,输出不一定是完整的长路径,但很可能是短文件名或路径别名。我希望当你使用这样一个函数时,目标是为调用者提供一个可用的、可靠的完整的、长的路径,因此我建议确保返回一个可用的、可靠的、完整的、长的绝对路径,以这样的方式,它是在各种Windows版本和架构之间移植的(再次是32/64位/WOW 64)。如何有效地做到这一点超出了这里的范围。
虽然这是现存最糟糕的Win32 API之一,但我还是希望你能有很多编码的乐趣。

2izufjch

2izufjch5#

我的例子是“如果一开始没有成功,就将缓冲区的长度加倍”方法的具体实现。它检索正在运行的可执行文件的路径,使用字符串(实际上是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"";
}
7vux5j2d

7vux5j2d6#

下面是另一个使用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;
}
fwzugrvs

fwzugrvs7#

这里有一个在Free Pascal(FPC)/Delphi中的实现,以防任何人需要它:

function GetExecutablePath(): UnicodeString;
const
  MAX_CHARS = 65536;
var
  NumChars, BufSize, CharsCopied: DWORD;
  pName: PWideChar;
begin
  // Poorly designed API...
  result := '';
  NumChars := 256;
  repeat
    BufSize := (NumChars * SizeOf(WideChar)) + SizeOf(WideChar);
    GetMem(pName, BufSize);
    CharsCopied := GetModuleFileNameW(0,  // HMODULE hModule
      pName,                              // LPWSTR  lpFilename
      NumChars);                          // DWORD   nSize
    if (CharsCopied < NumChars) and (CharsCopied <= MAX_CHARS) then
      result := UnicodeString(pName)
    else
      NumChars := NumChars * 2;
    FreeMem(pName, BufSize);
  until (CharsCopied >= MAX_CHARS) or (result <> '');
end;
8xiog9wr

8xiog9wr8#

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

332nm8kg

332nm8kg9#

我的方法是使用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应用程序)。但以防万一,有一个回退到上面已经描述的解决方案。对我来说似乎有点丑陋,但仍然完成了工作。

相关问题