Работа с INI-файлами на Ассемблере
Работа с INI-файлами на Ассемблере
Конфигурационные ini-файлы появились в самых первых версиях Windows. Изначально в них хранились только настройки Windows, а затем они стали использоваться для хранения параметров других приложений. Начиная с Windows 95, Microsoft объявил ini-файлы устаревшими и с тех пор предлагает использовать системный реестр для хранения всех настроек и данных программ. Лично я считаю, что приложения должны быть легко переносимыми между компьютерами, а также легко и полностью деинсталлироваться, поэтому внедрение в систему должно быть минимальным. Хранение всех настроек в ini-файле или в xml-файле в папке с программой - это, на мой взгляд, самое правильное решение, а в реестр нужно залезать только в случае крайней необходимости.
Несложная структура формата ini-файлов позволяет легко обрабатывать их программно и имеет достаточно понятный вид для чтения и изменения человеком. Работать с ini-файлами из приложения одно удовольствие: чтобы читать параметры из файла конфигурации достаточно всего трех функций GetPrivateProfileString (строковые данные), GetPrivateProfileInt (числовые данные) и GetPrivateProfileStruct (двоичные данные), а вот для записи данных обратно есть только две функции WritePrivateProfileString (строковые данные) и WritePrivateProfileStruct (двоичные данные). Отдельной функции для записи числовых значений по какой-то причине не предусмотрено. Но это все хорошо, когда вы обрабатываете ini-файл с известной структурой, когда в приложении заранее определены имена секций и названия ключей.
Пример INI-файла
А вот случай посложнее, когда в связи с особенностью работы приложения, структура ini-файла неизвестна. Или если известны имена секций, но количество и список ключей и их значений могут меняться. Парсить ini-файл с неизвестным содержимым самостоятельно - задача не самая тривиальная. При кажущейся простоте структуры, в файле могут быть комментарии, строки могут быть отформатированы разными способами, ключи и значения могут быть разделены как пробелами, так и табуляциями, да мало ли еще может быть вариантов. К счастью, разработчики WinAPI позаботились об этом, и система способна полностью парсить ini-файлы самостоятельно. Сегодня я расскажу, как на Ассемблере прочитать структуру секций, все ключи и их значения из произвольного ini-файла.
Сперва определим в сегменте данных переменные, в которые будут записаны все нужные нам значения.
Code (Assembler) : Убрать нумерацию
- section '.data' data readable writeable
- ; Список секций
- sections rb 1024
- ; Список ключений и значений
- keys rb 1024*32
- ; Имя секции
- sname rb 100h
- ; Название ключа
- key rb 100h
- ; Значение ключа
- value rb 100h
Список имен секций
Содержимое секций можно прочитать другой функцией - GetPrivateProfileSection. Она вернет список ключей и их значений в виде ASCIIZ-строк в формате "ключ=значение". Окончание списка также определяется нулевым байтом. Все комментарии удаляются, а строки, которые не соответствуют формату ini-файла, игнорируются. При этом максимальный размер полезного содержимого секции, как сказано в описании функции, не должен превышать 32 килобайта. Может быть это утверждение справедливо для более старых версий Windows, а на современных системах, как показала практика, штатными средствами совершенно спокойно обрабатываются файлы с размером секций в несколько мегабайт.
Список ключей и их значений
И вот этот список нам все-таки придется парсить самостоятельно. Имя ключа и его значение всегда разделено знаком равенства "=" без пробелов, независимо от того, как эта строчка была записана в исходном файле. Разработчики WinAPI и тут нам очень помогли. Ведь по сути все значения в ini-файле являются строками и то, как будет представлено их значение, зависит исключительно от функции, которой это значение будет запрошено. Это надо будет учитывать при ручной обработке данных из файла. Полностью код для парсинга ini-файла выглядит примерно так:
Code (Assembler) : Убрать нумерацию
- ; Получить список секций ini-файла
- invoke GetPrivateProfileSectionNames,sections,1024,ini_file
- ; Файл пустой?
- or eax,eax
- jz loc_scan_sections_done
- ; Указатель на список секций
- mov esi,sections
- loc_scan_sections:
- ; Конец списка секций?
- cmp byte [esi],0
- je loc_scan_sections_done
- ; Имя обрабатываемой секции
- mov edi,sname
- @@:
- lodsb
- stosb
- or al,al
- jnz @b
- push esi
- ;----------------------------------------------
- ; sname = имя секции
- ;----------------------------------------------
- ; Прочитать содержимое секции
- invoke GetPrivateProfileSection,sname,keys,1024*32,ini_file
- ; Секция пустая?
- or eax,eax
- jz loc_scan_keys_done
- ; Указатель на список ключений и значений
- mov esi,keys
- loc_scan_keys:
- ; Конец списка ключей?
- cmp byte [esi],0
- je loc_scan_keys_done
- ; Название ключа
- mov edi,key
- @@:
- lodsb
- cmp al,'='
- je @f
- stosb
- jmp @b
- @@:
- xor eax,eax
- stosb
- ; Значение ключа
- mov edi,value
- @@:
- lodsb
- stosb
- or al,al
- jnz @b
- ;----------------------------------------------
- ; key = название ключа
- ; value = значение ключа
- ;----------------------------------------------
- ; Следующий ключ
- jmp loc_scan_keys
- loc_scan_keys_done:
- ; Следующая секция
- pop esi
- jmp loc_scan_sections
- loc_scan_sections_done:
- ; Разбор ini-файла завершен
Еще немного полезной информации. Для того, чтобы сбросить кешированные данные на диск и сразу задействовать изменения после записи в ini-файл, надо вызвать функцию WriteProfileString или WritePrivateProfileString, передав им в качестве имени секции, наименования ключа и строки значения NULL:
Code (Assembler) : Убрать нумерацию
- ; Сбросить кешированные данные на диск
- invoke WriteProfileString, NULL, NULL, NULL
- invoke WritePrivateProfileString, NULL, NULL, NULL, ini_file
Code (Assembler) : Убрать нумерацию
- struct DECIMAL
- wReserved dw ?
- union
- struct
- scale db ?
- sign db ?
- ends
- signscale dw ?
- ends
- Hi32 dd ?
- union
- struct
- Lo32 dd ?
- Mid32 dd ?
- ends
- Lo64 dq ?
- ends
- ends
- struct VARIANT
- union
- struct
- vt dw ?
- wReserved rw 3
- union
- llVal dq ?
- lVal dd ?
- iVal dw ?
- bVal db ?
- ends
- ends
- decVal DECIMAL
- ends
- ends
- ; IID_IPropertyBag Interface
- struct IPropertyBag
- ; IUnknown
- QueryInterface dd ? ; 000h
- AddRef dd ? ; 004h
- Release dd ? ; 008h
- ; IPropertyBag
- Read dd ? ; 00Ch
- Write dd ? ; 010h
- ends
- ; GUID {55272A00-42CB-11CE-8135-00AA004BB851}
- IID_IPropertyBag \
- dd 055272A00h
- dw 042CBh
- dw 011CEh
- db 081h, 035h, 000h, 0AAh, 000h, 04Bh, 0B8h, 051h
- VT_BSTR = 8
- STGM_READWRITE = 0x02
Code (Assembler) : Убрать нумерацию
- invoke CoInitialize,0
- ; Полный путь до ini-файла
- invoke GetFullPathName,fname,MAX_PATH,ini_file,NULL
- ; Импортировать функции из shlwapi.dll
- invoke LoadLibrary,szlib
- mov [lib],eax
- ; SHCreatePropertyBagOnProfileSection
- invoke GetProcAddress,[lib],472
- mov [SHCreatePropertyBagOnProfileSection],eax
- ; Связать объект с секцией ini-файла
- stdcall [SHCreatePropertyBagOnProfileSection],ini_file,\
- sname,STGM_READWRITE,IID_IPropertyBag,ppBag
- ; Получить значение ключа
- mov eax,[ppBag]
- mov eax,[eax]
- ; Ожидаемый тип значения
- mov [vtVar.vt],VT_BSTR
- stdcall dword [eax+IPropertyBag.Read],[ppBag],\
- szKeyName1,vtVar,NULL
- ...
- ; [vtVar.lVal] -> указатель на строку со значением ключа
- ...
- ; Очистить выделенную для строки память
- invoke SysFreeString,[vtVar.lVal]
- ; Установить значение ключа или добавить новый
- mov eax,[ppBag]
- mov eax,[eax]
- ; Тип значения - указатель на строку
- mov [vtVar.vt],VT_BSTR
- ; Указатель на строку
- mov [vtVar.lVal],szVal
- stdcall dword [eax+IPropertyBag.Write],[ppBag],\
- szKeyName2,vtVar
- ; Удалить объект
- invoke CoUninitialize
В описании интерфейса IPropertyBag2 есть еще два интересных метода, а именно CountProperties для получения количества свойств в списке и GetPropertyInfo для получения информации. Велик соблазн реализовать с их помощью перебор всех ключей секции, но не тут-то было. При попытке вызова этих методов для объекта секции ini-файла, они завершаются с ошибкой, так как объект такого типа не поддерживает в полной мере интерфейс IPropertyBag2. Поэтому читать значения можно только для заведомо известных названий ключей.
Кроме методов интерфейсов в библиотеке shlwapi.dll есть еще несколько функций для работы с наборами параметров, в нашем случае это ключи ini-файлов. Как и основная функция, эти функции доступны только по ординалам. Вот их актуальный список с указанием ординала и протитипа.
493 - SHPropertyBag_ReadType (ppb, pszPropName, VARIANT* pv, VARTYPE vt);
494 - SHPropertyBag_ReadStr (ppb, pwzPropName, LPWSTR psz, int cch);
495 - SHPropertyBag_WriteStr (ppb, pwzPropName, LPCWSTR psz);
496 - SHPropertyBag_ReadLONG (ppb, pwzPropName, LONG* pl);
497 - SHPropertyBag_WriteLONG (ppb, pwzPropName, LONG l);
498 - SHPropertyBag_ReadBOOLOld (ppb, pwzPropName, BOOL* bDefault);
499 - SHPropertyBag_WriteBOOL (ppb, pwzPropName, BOOL fValue);
505 - SHPropertyBag_ReadGUID (ppb, pwzPropName, GUID* pguid);
506 - SHPropertyBag_WriteGUID (ppb, pwzPropName, const GUID* pguid);
507 - SHPropertyBag_ReadDWORD (ppb, pwzPropName, DWORD* pdw);
508 - SHPropertyBag_WriteDWORD (ppb, pwzPropName, DWORD dw);
512 - SHPropertyBag_ReadPIDL (ppb, pwzPropName, LPITEMIDLIST* ppidl);
513 - SHPropertyBag_WritePIDL (ppb, pwzPropName, LPCITEMIDLIST pidl);
520 - SHPropertyBag_ReadBSTR (ppb, pwzPropName, BSTR* pbstr);
521 - SHPropertyBag_ReadPOINTL (ppb, pwzPropName, POINTL* ppt);
522 - SHPropertyBag_WritePOINTL (ppb, pwzPropName, const POINTL* ppt);
523 - SHPropertyBag_ReadRECTL (ppb, pwzPropName, RECTL* prc);
524 - SHPropertyBag_WriteRECTL (ppb, pwzPropName, const RECTL* prc);
525 - SHPropertyBag_ReadPOINTS (ppb, pwzPropName, POINTS* ppt);
526 - SHPropertyBag_WritePOINTS (ppb, pwzPropName, const POINTS* ppt);
527 - SHPropertyBag_ReadSHORT (ppb, pwzPropName, SHORT* psh);
528 - SHPropertyBag_WriteSHORT (ppb, pwzPropName, SHORT sh);
529 - SHPropertyBag_ReadInt (ppb, pwzPropName, INT* piResult);
530 - SHPropertyBag_WriteInt (ppb, pwzPropName, INT iValue);
531 - SHPropertyBag_ReadStream (ppb, pwzPropName, IStream** ppstm);
532 - SHPropertyBag_WriteStream (ppb, pwzPropName, IStream* pstm);
534 - SHPropertyBag_ReadBOOL (ppb, pwzPropName, BOOL* pfResult);
535 - SHPropertyBag_Delete (ppb, pszPropName);
Если внимательно посмотреть, то можно заметить, что некоторые функции, в основном для чтения-записи числовых значений, практически дублируют друг друга. По-моему ситуация, когда надо прочитать значение именно конкретного байта, крайне редкая. Еще стоит обратить внимание на функции, которые работают со структурами, например, с POINT или RECT. Такие наборы данных хранятся в ini-файле в виде следующих записей:
[options]
; Структура POINT
my_pt.x=100
my_pt.y=200
; Структура RECT
my_rc.left=0
my_rc.top=0
my_rc.right=50
my_rc.bottom=95
Это очень удобно, так как достаточно будет запросить значение одного ключа, соответственно, это будут ключи my_pt и my_rc, чтобы загрузить в приложение уже заполненную структуру. GUID'ы записываются в соответствии с принятым шаблоном:
[options]
; GUID
my_guid={55272A00-42CB-11CE-8135-00AA004BB851}
Функции для чтения-записи строк работают с тем же особенностями, что и методы, то есть строковые значения в кавычках читаются вместе с кавычками. Булевые данные хранятся в виде числовых значений, -1 для TRUE и 0 для FALSE.
В списке есть две функции, заслуживающие отдельного внимания: SHPropertyBag_ReadType и SHPropertyBag_Delete. С помощью первой можно получить тип значения ключа до того, как запрашивать это значение. Вторая используется для удаления ключа. Вопреки названию функции, запись о ключе в ini-файле не удаляется, а только очищается его значение.
Code (Assembler) : Убрать нумерацию
- invoke CoInitialize,0
- ; Полный путь до ini-файла
- invoke GetFullPathName,fname,MAX_PATH,ini_file,NULL
- ; Импортировать функции из shlwapi.dll
- invoke LoadLibrary,szlib
- mov [lib],eax
- ; SHCreatePropertyBagOnProfileSection
- invoke GetProcAddress,[lib],472
- mov [SHCreatePropertyBagOnProfileSection],eax
- ; SHPropertyBag_WriteLONG
- invoke GetProcAddress,[lib],497
- mov [SHPropertyBag_WriteLONG],eax
- ; SHPropertyBag_ReadStr
- invoke GetProcAddress,[lib],494
- mov [SHPropertyBag_ReadStr],eax
- ; Связать объект с секцией ini-файла
- stdcall [SHCreatePropertyBagOnProfileSection],ini_file,\
- sname,STGM_READWRITE,IID_IPropertyBag,ppBag
- ; Получить строковое значение
- stdcall [SHPropertyBag_ReadStr],[ppBag],szKeyName1,buff,100h
- ; Установить числовое значение
- stdcall [SHPropertyBag_WriteLONG],[ppBag],szKeyName2,100500
- ; Удалить объект
- invoke CoUninitialize
Просмотров: 4277 | Комментариев: 5
Внимание! Статья опубликована больше года назад, информация могла устареть!
Комментарии
Отзывы посетителей сайта о статье
ManHunter
(19.05.2023 в 16:33):
Добавил описание работы с функциями SHPropertyBag_*
ManHunter
(18.05.2023 в 21:16):
Добавил в статью информацию о работе с ini-файлами с помощью COM, архив обновлен.
ManHunter
(21.06.2016 в 00:24):
Если запись в папку с программой по какой-то причине закрыта, то можно хранить настройки в пользовательской папке %USERPROFILE% или %LOCALAPPDATA%, не обязательно же сразу лезть в каталог винды. Экспорт-импорт из/в реестр тоже не панацея, при внезапно рухнувшей системе доступ к реестру может оказаться если не невозможен, то по крайней мере сильно затруднен. А если программа хранит настройки у себя в файле, то после переустановки системы или при переносе программы на другой комп достаточно будет только заново создать ярлычки для ее запуска. При хранении настроек в пользовательской папке легко решается вопрос с персональными настройками для разных учетных записей, если программа подразумевает для них какой-то индивидуальный режим работы. Резервное копирование в автоматическом режиме тоже гораздо легче делать поиском и архивированием *.ini, чем прописыванием в бэкапилке и поддержанием в актуальном состоянии списка из 100500 веток реестра. Короче, сплошные плюсы :)
Реестр must die!
user
(20.06.2016 в 19:42):
--Добавлено--
.. когда-то сделал собственную библиотечку на Си (для DOS) с реализацией аналогов Get/Wirite-PrivateProfileInt и Get/Write**String, но, как это часто бывало, попользовался ею сам всего пару раз и забросил - слишком громоздкая оказалась штука, хотя работала вполне нормально ..
С WinAPI всё гораздо проще.
.. когда-то сделал собственную библиотечку на Си (для DOS) с реализацией аналогов Get/Wirite-PrivateProfileInt и Get/Write**String, но, как это часто бывало, попользовался ею сам всего пару раз и забросил - слишком громоздкая оказалась штука, хотя работала вполне нормально ..
С WinAPI всё гораздо проще.
user
(20.06.2016 в 19:29):
Немного поразглагольствую на тему, с позволенья.
Действительно, использование собственного файла настроек выглядит предпочтительным во всех случаях, кроме, разве что, случая запуска программы с защищённого от записи носителя.
Но и в этом случае существует популярный стандартный метод работы со своим файлом настроек - его часто размещали в каталоге %WINDIR%. Особенно это было популярно во времена WIN16. Там (в %WINDIR%) обычно накапливались эти самые INI-файлы в приличных количествах от разных программ.
Такой способ тоже имеет недостаток - при запуске разных версий софта он будет писать/читать INI-файл не своей версии.
Примером может служить популярный wave-редактор CoolEdit. У этого редактора в INI-файле хранились регистрационные данные, так что головняк был постоянный.
Решалось запуском CoolEdit'а из пакетного файла, который прежде копировал свой INI из каталога программы в %WINDIR%.
Неудобство заключалось в том, что вновь сохранённые настройки перезаписывались старым файлом при следующем запуске.
Еще один неплохой способ хранения настроек в популярном файловом менеджере FAR - настройки хранятся в реестре, но их можно оттуда экспортировать в reg-файл c помощью самой программы. Для сохранения/переноса на другую машину.
В общем, идеального способа нет. Вернее, идеальным можно считать, если в программе предусмотрен выбор способа хранения своих настроек несколькими (двумя) из перечисленных способов.
Симпатичным представляется вариант, когда программа использует свой INI-файл в каталоге запуска, иначе INI-файл в каталоге %WINDIR%, иначе ключи в реестре. И есть возможность выбрать способ при сохренении настроек.
Ну, как-то так.
Действительно, использование собственного файла настроек выглядит предпочтительным во всех случаях, кроме, разве что, случая запуска программы с защищённого от записи носителя.
Но и в этом случае существует популярный стандартный метод работы со своим файлом настроек - его часто размещали в каталоге %WINDIR%. Особенно это было популярно во времена WIN16. Там (в %WINDIR%) обычно накапливались эти самые INI-файлы в приличных количествах от разных программ.
Такой способ тоже имеет недостаток - при запуске разных версий софта он будет писать/читать INI-файл не своей версии.
Примером может служить популярный wave-редактор CoolEdit. У этого редактора в INI-файле хранились регистрационные данные, так что головняк был постоянный.
Решалось запуском CoolEdit'а из пакетного файла, который прежде копировал свой INI из каталога программы в %WINDIR%.
Неудобство заключалось в том, что вновь сохранённые настройки перезаписывались старым файлом при следующем запуске.
Еще один неплохой способ хранения настроек в популярном файловом менеджере FAR - настройки хранятся в реестре, но их можно оттуда экспортировать в reg-файл c помощью самой программы. Для сохранения/переноса на другую машину.
В общем, идеального способа нет. Вернее, идеальным можно считать, если в программе предусмотрен выбор способа хранения своих настроек несколькими (двумя) из перечисленных способов.
Симпатичным представляется вариант, когда программа использует свой INI-файл в каталоге запуска, иначе INI-файл в каталоге %WINDIR%, иначе ключи в реестре. И есть возможность выбрать способ при сохренении настроек.
Ну, как-то так.
Добавить комментарий
Заполните форму для добавления комментария