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

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

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

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

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

Ответ 1

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

Ответ 2

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

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

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

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

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

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

MSDN имеет это сказать о длине пути в статье под названием Имена файлов, пути и пространства имен:

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

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

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

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

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

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

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

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

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

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

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

Ответ 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). Как это сделать эффективно, это не входит в сферу применения здесь.

Хотя это один из худших API-интерфейсов Win32, тем не менее, я желаю вам большой радости от кодирования.

Ответ 4

Использование

extern char* _pgmptr

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

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

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

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

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

Любой, кто знает, как _pgmptr инициализируется? Если бы у SO была поддержка последующих вопросов, я бы разместил этот вопрос в качестве продолжения.

Ответ 5

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

Ответ 6

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

Ответ 7

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

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