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

StackOverflow https://stackoverflow.com/questions/805814

Вопрос

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

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

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

(Трюк с использованием нуля в качестве размера буфера не работает для этого API — я уже пробовал раньше)

Это было полезно?

Решение

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

Другие советы

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

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

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

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

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

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

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

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

В API Windows (за некоторыми исключениями, обсуждаемыми в следующих параграфах) максимальная длина для пути - это 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>".(Персонажи < > используются здесь для визуальной ясности и не могут быть частью действительной строки пути.)

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

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

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

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

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

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

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

Хотя 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-бит/WOW64).Вопрос о том, как сделать это эффективно, выходит за рамки данной статьи.

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

С использованием

extern char* _pgmptr

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top