Как я могу рассчитать полный размер буфера для GetModuleFileName?

GetModuleFileName() принимает в качестве входных данных буфер и размер буфера; однако его возвращаемое значение может только сказать нам, сколько символов скопировано, и если размер не достаточен (ERROR_INSUFFICIENT_BUFFER).

Как определить реальный необходимый размер буфера для хранения полного имени файла для GetModuleFileName()?

Большинство людей используют MAX_PATH но я помню, что путь может превышать это (260 по умолчанию)...

(Уловка использования нуля, поскольку размер буфера не работает для этого API - я уже пробовал ранее)

9 ответов

Решение

Реализуйте разумную стратегию для увеличения буфера, например, начните с MAX_PATH, затем увеличьте каждый последующий размер в 1,5 раза (или в 2 раза за меньшее количество итераций), чем предыдущий. Итерируйте, пока функция не завершится успешно.

Обычный рецепт состоит в том, чтобы назвать его установкой размера на ноль, и он гарантированно потерпит неудачу и предоставит размер, необходимый для выделения достаточного буфера. Выделите буфер (не забывайте место для нулевого завершения) и вызовите его во второй раз.

Во многих случаях MAX_PATH достаточно, потому что многие файловые системы ограничивают общую длину имени пути. Тем не менее, можно создать легальные и полезные имена файлов, которые превышают MAX_PATHтак что, вероятно, это хороший совет для запроса необходимого буфера.

Не забудьте в конце концов вернуть буфер из распределителя, который его предоставил.

Редактировать: Фрэнсис указывает в комментарии, что обычный рецепт не работает дляGetModuleFileName(), К сожалению, Фрэнсис абсолютно прав в этом вопросе, и мое единственное оправдание состоит в том, что я не пошел, чтобы проверить это, прежде чем предлагать "обычное" решение.

Я не знаю, о чем думал автор этого API, за исключением того, что, возможно, когда он был представлен, MAX_PATH действительно был самый большой возможный путь, делая правильный рецепт легким. Просто выполните все манипуляции с именами файлов в буфере длиной не менее MAX_PATH персонажи.

О, да, не забывайте, что имена путей с 1995 года или около того допускают символы Юникода. Поскольку Unicode занимает больше места, любому имени пути может предшествовать \\?\ явно просить, чтобы MAX_PATH ограничение на длину его байта будет снято для этого имени. Это усложняет вопрос.

В MSDN говорится о длине пути в статье под названием " Имена файлов, пути и пространства имен":

Максимальная длина пути

В Windows API (с некоторыми исключениями, обсуждаемыми в следующих параграфах) максимальная длина пути равна MAX_PATH, который определяется как 260 символов. Локальный путь структурирован в следующем порядке: буква диска, двоеточие, обратная косая черта, компоненты, разделенные обратной косой чертой, и завершающий нулевой символ. Например, максимальный путь на диске D равенD:\<some 256 character path string><NUL>" где "<NUL>"представляет невидимый завершающий нулевой символ для текущей системной кодовой страницы. (Символы <> используются здесь для наглядности и не могут быть частью допустимой строки пути.)

Примечание. Функции ввода-вывода файлов в Windows API convert "/"до"\"как часть преобразования имени в имя в стиле NT, кроме случаев использования"\\?\Префикс, как описано в следующих разделах.

Windows API имеет много функций, которые также имеют версии Unicode, чтобы разрешить путь расширенной длины для максимальной общей длины пути 32 767 символов. Этот тип пути состоит из компонентов, разделенных обратной косой чертой, каждая из которых соответствует значению, возвращенному в lpMaximumComponentLength параметр GetVolumeInformation функция. Чтобы указать путь расширенной длины, используйте "\\?\"Префикс. Например,"\\?\D:\<very long path>". (Персонажи <> используются здесь для наглядности и не могут быть частью допустимой строки пути.)

Примечание. Максимальный путь в 32 767 символов является приблизительным, поскольку\\?\"префикс может быть расширен системой до более длинной строки во время выполнения, и это расширение применяется к общей длине.

"\\?\"Префикс также можно использовать с путями, созданными в соответствии с универсальным соглашением об именах (UNC). Чтобы указать такой путь с помощью UNC, используйте"\\?\UNC\"Префикс. Например,"\\?\UNC\server\share", где"server"- это имя машины, а" share "- это имя общей папки. Эти префиксы не используются как часть самого пути. Они указывают, что путь должен быть передан в систему с минимальными изменениями., что означает, что вы не можете использовать косую черту для представления разделителей пути или точку для представления текущего каталога. Также вы не можете использовать "\\?\"префикс с относительным путем, поэтому относительные пути ограничены MAX_PATH символы, как указано ранее для путей, не использующих "\\?\" префикс.

При использовании API для создания каталога указанный путь не может быть настолько длинным, что вы не сможете добавить имя файла 8.3 (то есть имя каталога не может превышать MAX_PATH минус 12).

Оболочка и файловая система имеют разные требования. Можно создать путь с помощью Windows API, который пользовательский интерфейс оболочки может не обработать.

Так что простым ответом было бы выделить буфер размера MAX_PATH, найдите имя и проверьте на наличие ошибок. Если это подходит, вы сделали. В противном случае, если это начинается с "\\?\", получите буфер размером 64 КБ или около того (приведенная выше фраза" максимальный путь из 32 767 символов приблизителен ", поэтому я оставляю некоторые детали для дальнейшего изучения) и попробуйте снова.

Переполненная MAX_PATH но не начиная с "\\?\"Похоже, что это не может случиться". Опять же, что делать, это деталь, с которой вам придется иметь дело.

Может также быть некоторая путаница по поводу того, каково ограничение длины пути для имени сети, которое начинается "\\Server\Share\msgstr "не говоря уже об именах из пространства имен объектов ядра, которые начинаются с"\\.\Msgstr "В приведенной выше статье ничего не сказано, и я не уверен, сможет ли этот API вернуть такой путь.

С помощью

extern char* _pgmptr

может работать.

Из документации GetModuleFileName:

Глобальная переменная _pgmptr автоматически инициализируется по полному пути исполняемого файла и может быть использована для получения полного пути к исполняемому файлу.

Но если я прочитал о _pgmptr:

Когда программа не запускается из командной строки, _pgmptr может быть инициализировано именем программы (базовое имя файла без расширения имени файла) или именем файла, относительным путем или полным путем.

Кто-нибудь, кто знает, как инициализируется _pgmptr? Если бы SO поддерживал последующие вопросы, я бы опубликовал этот вопрос в качестве последующего.

Хотя API является доказательством плохого дизайна, решение на самом деле очень простое. Простой, но грустный, так должно быть, потому что это в некоторой степени снижает производительность, поскольку может потребовать многократного выделения памяти. Вот некоторые ключевые моменты к решению:

  • Вы не можете полагаться на возвращаемое значение между различными версиями Windows, так как оно может иметь разную семантику в разных версиях Windows (например, XP).

  • Если предоставленный буфер слишком мал для хранения строки, возвращаемое значение - это количество символов, включая терминатор 0.

  • Если предоставленный буфер достаточно велик, чтобы содержать строку, возвращаемое значение - это количество символов, исключая 0-терминатор.

Это означает, что если возвращаемое значение в точности равно размеру буфера, вы все равно не знаете, успешно оно или нет. Там может быть больше данных. Или нет. В конце концов, вы можете быть уверены в успехе, только если длина буфера больше требуемой. К сожалению...

Итак, решение состоит в том, чтобы начать с небольшого буфера. Затем мы вызываем GetModuleFileName, передавая точную длину буфера (в TCHAR) и сравнивая с ней возвращаемый результат. Если возвращаемый результат меньше, чем длина нашего буфера, он успешен. Если возвращаемый результат больше или равен длине нашего буфера, мы должны повторить попытку с большим буфером. Промыть и повторить до готовности. Когда мы закончим, мы сделаем копию строки (strdup/wcsdup/tcsdup) буфера, очистим и вернем копию строки. Эта строка будет иметь правильный размер размещения, а не вероятные издержки из нашего временного буфера. Обратите внимание, что вызывающая сторона отвечает за освобождение возвращаемой строки (память mallocs 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-битным /WOW64. Кроме того, вывод не обязательно является полным, длинным путем, но вполне может быть коротким именем файла или иметь псевдоним пути. Я ожидаю, что когда вы используете такую ​​функцию, цель состоит в том, чтобы предоставить вызывающему абоненту полезный, надежный полный, длинный путь, поэтому я предлагаю действительно обеспечить возвращение полезного, надежного, полного, длинного абсолютного пути таким образом, чтобы он переносим между различными версиями Windows и архитектурами (опять же 32/64-bit/WOW64). Как сделать это эффективно, выходит за рамки здесь.

Хотя это один из худших из существующих Win32 API, я все же желаю вам радости от программирования.

Мой пример - конкретная реализация подхода "если сначала у вас ничего не получится, удвойте длину буфера". Он получает путь к исполняемому исполняемому файлу, используя строку (на самом деле 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"";
}

Вот еще одно решение с 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;
}

Вот реализация в 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;

Windows не может правильно обрабатывать пути длиннее 260 символов, поэтому просто используйте MAX_PATH. Вы не можете запустить программу, имеющую путь более длинный, чем MAX_PATH.

Мой подход к этому - использовать 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-консольное приложение), пока. Но на всякий случай есть запасной вариант решения, которое было описано выше. Мне это кажется немного уродливым, но все же работа сделана.

Другие вопросы по тегам