Общие принципы работы с ресурсами.
В этом разделе я расскажу о том как работать с ресурсами любого типа на уровне API, т.е. без использования функций, предназначенных для работы только с какими-либо определенными типами ресурсов. В предыдущих разделах я предоставлял готовые функции вида
_LoadResource... для загрузки ресурса того или иного типа, а сейчас я постараюсь более подробно рассказать о том, как собственно они работают. Если вас в данный момент это не особо интересует, то можете смело пропустить этот раздел.
Структуры.
Перед тем, как говорить о Windows API, я должен рассказать о таком типе данных, как структура. О том для чего она вообще нужна в AutoIt и как с ней работать. Итак, начну с того, что в AutoIt все переменные являются типом Variant, т.е. такие переменные могут содержать данные любого типа, к тому же, тип данных может динамически изменяться в ходе выполнения программы. С одной стороны, это очень удобно, поскольку вам не приходится заботиться о соответствии типа переменной типу данных, которые присваиваются этой переменной. Да и в большинстве случаев это сильно сокращает количество переменных в вашем коде. Например, вы можете написать так:
Dim $Var[4]
$Var = 0
$Var = "0"
В этом примере, после выполнения первой строки, переменная $Var будет представлять собой одномерный массив из четырех элементов. Во второй строке, тип $Var изменится на целочисленный. В третьей, $Var будет уже строкового типа (т.к. присутствуют кавычки). Как видно, все очень просто, и наверное мало кто задумывается о том, какого типа в данный момент является та или иная переменная. С другой стороны, это требует более внимательного подхода к написанию кода, т.к. при случайном изменении типа переменной, могут возникнуть серьезные ошибки, вплоть до "вылета" программы или потере каких-нибудь данных на диске. Например, если вы напишите так:
Dim $Var[4]
$Var = 0
$Var[0] = 0
то произойдет ошибка во время выполнения программы, т.к. в третьей строке, переменная $Var уже не будет являться массивом, а будет иметь целочисленный тип и содержать значение 0.
Все это конечно не так страшно, просто нужно аккуратнее и внимательнее писать код, но здесь есть и еще один более серьезный недостаток. Не знаю, связано ли это с типом Variant или нет, но адресацией (расположением в памяти) всех переменных занимается непосредственно AutoIt и только он. Пользователю не предоставляется никаких инструментов, позволяющих вделять или освобождать память для переменных. Вы даже не можете узнать адрес по которому находятся данные, связанные с вашей переменной. Например в Delphi для этого служит префикс "^", в PureBasic - "*", а в AutoIt ничего нет. Конечно, если вы будете писать скрипты, используя только нативные функции AutoIt, то это вряд ли вам понадобится, но в случае необходимости использовать функции из Windows API, без этого ну никак не обойтись. И вот именно для этого в AutoIt и были введены структуры.
Итак, что же представляет собой структура и с чем ее едят? Структура - это тип данных, объединяющий несколько переменных одного или разных, но строго фиксированных, типов. Другими словами, структура представляет собой зарезервированную область памяти заданного размера и условно поделенная на отдельные участки. Размер каждого такого участка равен размеру соответствующего иму типа. Таким образом, структура не может состоять из переменных типа Variant. Все переменные, включенные в структуру, должны быть однозначно определенного типа и занимать соответствующий этому типу объем памяти. Общий размер занимаемой структурой памяти есть величина постоянная и равна сумме размеров всех включенных в структуру переменных. Еще нужно сказать, что структура располагается в памяти непрерывно, т.е. все данные, которые содержит структура, следуют в памяти друг за другом, начиная с заданного адреса. Из этого следует, что максимальный размер структуры равен максимальной нефрагментированной области памяти, доступной программе. Например по умолчанию в 32-битной Windows, все приложения могут использовать до 2 ГБ памяти. На деле же будет возможно создать структуру размером не более ~1.5 ГБ, в зависимости от количества памяти в компьютере и количества запущенных в данный момент приложений. В случае перебора, вы получите ошибку "Error allocating memory".
Ну, с тем, что такое структура, мы вроде бы разобрались. Теперь давайте поговорим о том, как с ними работать в AutoIt. Для этого существует всего несколько функций:
DllStructCreate
DllStructGetData
DllStructGetPtr
DllStructGetSize
DllStructSetData
Подробно о них написано в документации по AutoIt. Я расскажу здесь лишь самое необходимое. Создается структура с помощью функции
DllStructCreate(). В качестве параметра передается строка, в которой перечисляются условные названия типов данных, из которых собственно и должна состоять структура, разделенные символом ";". Допустимые названия типов для создания структур в AutoIt перечислены в таблице в описании к этой функции (см. выше). Что хорошо, все названия типов полностью соответствуют их названиям в C/C++ и соответственно в
MSDN, что безусловно поможет в будущем избежать путаницы, читая описания к API функциям в том же MSDN. Каждый тип данных резервирует строго определенный размер памяти. Вот простой пример создания структуры на AutoIt:
$tStruct = DllStructCreate("int;int")
Здесь создается структура, состоящая из двух последовательно расположенных элементов целочисленного знакового типа
INT. Посмотрев в таблицу допустимых типов, вы увидите, что тип INT требует для хранения данных всего 4 байта памяти. Как нетрудно догадаться, размер всей структуры будет равен 4 + 4 = 8 байт. Для удобства работы со структурами, вы можете для каждой переменной в структуре указать уникальное (в пределах этой структуры) имя, например:
$tStruct = DllStructCreate("int X;int Y")
Разница между этим и предыдущим примерами заключается лишь в том, что в первом случае мы можем обращаться к переменным (полям) структуры только по их порядковому номеру (в отличии от массивов, где первый элемент имеет индекс 0, в структурах всегда нумерация начинается с 1), а во втором случае, помимо индекса, мы можем обращаться к полям еще и по их именам (в данном случае
"X" и
"Y"). Если у вас последовательно расположено несколько переменных одного типа и позволяет логика самой структуры, то можно их записать как "массив". Например предыдущий пример можно было бы записать следующим образом:
$tStruct = DllStructCreate("int[2]")
или
$tStruct = DllStructCreate("int XY[2]")
В обоих случаях будет выделена память размером 8 байт, но только здесь мы должны уже обращаться к полю
"XY", используя еще и порядковый номер в "массиве", т.е. номер самой переменной "XY" будет всегда равен 1, т.к. стоит первая в структуре, а индекс, составляющих ее элементов - 1 или 2. Заметьте, что индекс здесь тоже начинается не с 0, как в массивах, а с 1.
ВАЖНО. При создании структуры всегда очень аккуратно перечисляйте типы данных, хорошо представляя, сколько каждый из них требует памяти и в какой последовательности они должны располагаться в структуре. В противном случае, это может стать причиной ряда "необъяснимых" ошибок или привести к краху вашей программы, а то и того хуже...
Как я уже сказал, при создании любой структуры, для нее будет выделен из стека необходимый объем памяти. В данном случае, это конечно ничтожно мало (8 байт), но ведь размер структуры может быть и гораздо больше. И здесь вся ответственность за освобождение используемой структурой памяти ложится непосредственно на вас. Если вы не будите следить за этим и своевременно освобождать неиспользуемую память, то возможно очень скоро увидите на экране надпись типа "Error allocating memory". Поэтому, хорошей идеей будет уничтожить структуру, освобождая тем самым занимаемую ей память, сразу после того, как она (структура) станет не нужна. Никаких функций для этого не требуется, здесь как раз используется свойство переменных типа Variant, т.е. достаточно просто изменить тип переменной, содержащей структуру. Другими словами, присвоить переменной какое-нибудь значение (как правило 0), но можно вообще что угодно, хоть массив или другую структуру для этой переменной создать. В любом случае, структура будет автоматически уничтожена (естественно, все данные, находящиеся в структуре, при этом будут потеряны), и освобожден соответствующий ей объем памяти. Если структура была создана внутри какой-либо функции как локальная переменная, то ее можно и не уничтожать, т.к. при завершении функции, это будет сделано автоматически. Вот простой пример, демонстрирующий как все это работает:
$tStruct = DllStructCreate('byte[1073741824]')
MsgBox(64 + 262144, "", "Создана структура размером 1 ГБ.")
$tStruct = 0
MsgBox(64 + 262144, "", "Структура уничтожена.")
Здесь создается простая структура, состоящая из последовательно расположенных элементов типа
BYTE. Размер этой структуры будет равен сумме размеров всех ее элементов (тип BYTE, как нетрудно догадаться, требует 1 байт памяти), т.е. 1 * 1073741824 = 1073741824 байтов или 1 ГБ. Соответственно и объем памяти, необходимый для создания такой структуру будет равен 1 ГБ. Если вы откроете "Диспетчер задач", то увидите, как будет меняться используемый процессом AutoIt.exe объем памяти. Ну, если на вашем компьютере установлено менее 2 ГБ памяти, то скорее всего, вы увидите сообщение типа "Error allocating memory". В этом случае уменьшите размер структуры, например до 512 МБ (536870912 вместо 1073741824).
Все, что мы до сих пор делали, с практической точки зрения ничем не отличается от объявления простых переменных, только более запутанно, и смысл этого на первый взгляд неочевиден. Так оно и было бы, если бы не одно очень полезное свойство структур - их можно объединять. Не добавлять одну к другой, (это физически нельзя сделать, т.к. структура должна занимать непрерывный участок памяти, а две разные структуры будут располагаться в разных областях адресного пространства), а создавать несколько структур по одному и тому же адресу в памяти. Какой в этом смысл? Здесь главное понять, что в памяти компьютера никаких типов данных не существует, и все они располагаются в виде последовательности отдельных байт (минимальной единице информации, к которой непосредственно приводятся все объёмы данных, только не говорите сейчас о битах). А как будет использоваться эта последовательность байт зависит только от нас. Например, если мы создали структуру вида
"int", которая занимает 4 байта памяти, а значения для нее лежат в пределах от -2147483648 до 2147483647, то мы можем на ее же месте (по тому же адресу) создать другую структуру вида
"short;short" или
"short[2]", которая тоже будет занимать 4 байта памяти (тип
SHORT требует 2 байта памяти), но только трактоваться эта структура будет по другому, уже ни одно, а два целых знаковых числа, но с диапазонами от –32768 до 32767, что соответствует 16-битному или 2-байтному знаковому числу. Или можем пойти дальше и создать поверх последней структуры, структуру типа
"byte;byte;byte;byte" или
"byte[4]". Размер такой структуры, как видно, тоже равен 4 байтам, но данные в ней представляют собой 4 последовательных числа с диапазонами от 0 до 255 (8-битное или 1-байтное беззнаковое число). И все эти три структуры будут содержать одни и теже данные (байты), но только по-разному их использовать. Естественно, физически будет создана только первая структура ("int"), а остальные будут использовать уже выделенную под нее память по тому же адресу. И, соответственно, освобождать занимаемую память, нужно только для первой структуры. Вот наглядный пример с комментариями, демонстрирующий все вышесказанное:
; Создаем структуру $tInt вида "int" (4 байта)
$tInt = DllStructCreate("int")
; Записываем данные в структуру
DllStructSetData($tInt, 1, 0x11223344)
; Читаем данные из структуры $tInt и смотрим результат в консоли
$iInt = DllStructGetData($tInt, 1)
ConsoleWrite("| " & Hex($iInt) & " |" & @CR)
;----------------------------------------
; Получаем адрес структуры $tInt
$pInt = DllStructGetPtr($tInt)
; Создаем структуру $tShort вида "short[2]" (2 * 2 = 4 байта) по адресу структуры $tInt
$tShort = DllStructCreate("short[2]", $pInt)
; Читаем данные из структуры $tShort и смотрим результат в консоли
ConsoleWrite("| ")
For $i = 1 To 2
$iShort = DllStructGetData($tShort, 1, $i)
ConsoleWrite(Hex($iShort, 4) & " | ")
Next
ConsoleWrite(@CR)
;----------------------------------------
; Получаем адрес структуры $tShort
$pShort = DllStructGetPtr($tShort)
; Создаем структуру $tByte вида "byte[4]" (1 * 4 = 4 байта) по адресу структуры $tShort
$tByte = DllStructCreate("byte[4]", $pShort)
; Читаем данные из структуры $tByte и смотрим результат в консоли
ConsoleWrite("| ")
For $i = 1 To 4
$iByte = DllStructGetData($tByte, 1, $i)
ConsoleWrite(Hex($iByte, 2) & " | ")
Next
ConsoleWrite(@CR)
;----------------------------------------
$tInt = 0
Если вы запустите на выполнение этот код, то увидите в консоли (вы же используете SciTE, не правда ли) следующую картину:
| 11223344 |
| 3344 | 1122 |
| 44 | 33 | 22 | 11 |
Здесь хорошо видно, что как мы записали в самом начале кода в структуру
$tInt значение 0x11223344, так оно никуда и не делось, т.к. все последующие структуры (
$tShort и
$tByte) есть суть структуры $tInt, и просто "накладывались" на нее, не затрагивая самих данных. Но если присмотреться внимательнее к полученному результату, то можно обнаружить очень интересную особенность, а именно то, что данные в памяти располагаются задом на перед. Не спрашивайте меня почему так, возможно тот, кто это придумал, был китайцем... Но факт остается фактом, так что примите это за аксиому и запомните, что все данные в пределах одного типа располагаются в памяти, начиная с младшего байта. Такой порядок байт называется
little-endian, или "интеловским". Но как ни странно, особо заморачиваться с этим не приходится, т.к. все это происходит на аппаратном уровне. Например в последнем примере, когда мы читаем данные из структуры $tInt, мы ведь не переворачиваем с ног на голову все четыре байта данных, да и вообще ничего не делаем. Просто прочитали число и вывели его в консоль. И результате получили тоже 0x11223344, а не 0x44332211. А все потому, что использовали тот же тип данных, что и при записи в структуру, и за нас это было сделано автоматически. Другое дело, когда мы читаем 4-байтовое число как два 2-байтовых или четыре 1-байтовых числа. В этом случае уже нужно вспоминать о порядке располажения байт в памяти. Во второй строке полученного результата, байты тоже идут в обратной последовательности, но только в пределах 2 байт (тип SHORT). Т.е. при операциях чтения (записи) из памяти данных любого типа, расположение байт в них меняет направление.
Теперь настало время рассказать об оставшихся функциях, предназначенных для работы со структурами, тем более я их уже использовал в вышеприведенном примере. Для записи данных в структуру используется функция
DllStructSetData(), для чтения -
DllStructGetData(). Когда вы записываете какие-либо данные в структуру, то следите за тем, чтобы эти данные соответствовали типу элемента структуры, в противном случае, результат может быть непредсказуемым. AutoIt не контролирует соответствие этих типов, и в случае ошибки, в структуре может оказаться "каша". Вот основные способы записи данных в структуру, в зависимости от способа ее создания:
$tStruct = DllStructCreate("int;int")
DllStructSetData($tStruct, 1, 400)
DllStructSetData($tStruct, 2, 250)
;----------------------------------------
$tStruct = DllStructCreate("int[2]")
DllStructSetData($tStruct, 1, 400, 1)
DllStructSetData($tStruct, 1, 250, 2)
;----------------------------------------
$tStruct = DllStructCreate("int X;int Y")
DllStructSetData($tStruct, "X", 400)
DllStructSetData($tStruct, "Y", 250)
; или
DllStructSetData($tStruct, 1, 400)
DllStructSetData($tStruct, 2, 250)
;----------------------------------------
$tStruct = DllStructCreate("int XY[2]")
DllStructSetData($tStruct, "XY", 400, 1)
DllStructSetData($tStruct, "XY", 250, 2)
; или
DllStructSetData($tStruct, 1, 400, 1)
DllStructSetData($tStruct, 1, 250, 2)
Чтение данных из структуры происходит аналогичным образом. Если элемент структуры объявлен как "массив", например
"byte[n]",
"char[n]" или
"wchar[n]", то допускается записывать данные в такие элементы следующим образом:
$bData = Binary("0x0123456789ABCDEF")
; Создаем структуру вида "byte[n]", размер котрой (n) равен размеру двоичных данных в переменной $bData
$tData = DllStructCreate("byte[" & BinaryLen($bData) & "]")
; Записываем сразу все n байт двоичных данных из переменной $bData в структуру $tData
DllStructSetData($tData, 1, $bData)
; Читаем сразу все n байт двоичных данных из структуры $tData
$bData = DllStructGetData($tData, 1)
ConsoleWrite($bData & @CR)
;----------------------------------------
$sData = "Общие принципы работы с ресурсами"
; Создаем структуру вида "wchar[n]", размер котрой (n) равен количеству символов в Unicode строке $sData + 1 (+1, т.к. нуль-терминированная строка)
$tData = DllStructCreate("wchar[" & (StringLen($sData) + 1) & "]")
; Записываем всю строку целиком в структуру $tData
DllStructSetData($tData, 1, $sData)
; Читаем всю строку целиком из структуры $tData
$sData = DllStructGetData($tData, 1)
ConsoleWrite($sData & @CR)
;----------------------------------------
$tData = 0
0x0123456789ABCDEF
Общие принципы работы с ресурсами
Обратите внимание на то, что в случае записи в структуру строки, необходимо, чтобы размер структуры был на один символ (2 байта, т.к. тип
WCHAR является Unicode символом и требует 2 байта памяти) больше, т.к. все строки в памяти компьютера представляются как
нуль-терминированные строки или ASCIZ (строки, последним символом в которых должен быть символ с кодом 0, обозначающий конец строки).
Функция
DllStructGetSize() позволяет получить размер (в байтах) созданной ранее структуры. Не подсчитывать же нам всегда размер самостоятельно, а структуры бывают далеко не такие простые, как в этом примере, и могут состоять из 10 и более элементов разного типа. Пользоваться этой функцией очень просто, в качестве единственного параметра передается переменная, содержащая созданную ранее с помощью функции
DllStructCreate() структуру. И наконец самая необходимая функция -
DllStructGetPtr(), которая возвращает адрес (указатель) созданной ранее структуры в памяти, и именно этот адрес требуется для вызова большинства функций из Windows API. Вызов
DllStructGetPtr() аналогичен
DllStructGetSize() и не представляет из себя ничего сложного. Еще есть функция
IsDllStruct(), которая позволяет определить, является ли в данный момент указанная переменная структурой или нет. Эту функцию полезно использовать для обнаружения ошибки при создании структуры, но обычно этого не делают, т.к. вид структуры известен заранее и никаких ошибок возникать не должно, если структура изначально составлена правильно. Да и всегда можно проверить состояние флага @error сразу после вызова
DllStructCreate().
Структуры в AutoIt используются в основном для вызова API функций и в обработчиках WM-сообщений (см. "Курсоры"). Даже названия (префикс) функций для работы со структурами говорят сами за себя. Разработчики как бы намекают нам, что все это добро предназначено именно для вызова функций из DLL. В принципе, так оно и есть. Трудно представить, для чего еще в AutoIt можно приспособить структуры. Да, конечно можно в них хранить какие-нибудь данные, но это крайне не рекомендуется (со слов разработчиков), лучше для этой цели использовать массивы. Хотя, в некоторых случаях структуры могут быть полезны для преобразования данных из одного типа в другой, но эта тема для отдельного разговора. В заключении хочу привести небольшой пример, наглядно демонстрирующий все то, о чем я здесь написал.
Попробуем определить, сколько в данный момент присутствует физической памяти в системе, и сколько из нее доступно (свободно) приложениям. Да, конечно мы можем получить эту информацию, воспользовавшись AutoIt функцией
MemGetStats(), и это правильно, но поскольку мы здесь говорим о структурах, то давайте ради закрепления материала используем для этого Windows API. Итак, покопавшись в MSDN, узнаем, что интересующую нас информацию о состоянии памяти в системе можно получить с помощью функции
GlobalMemoryStatusEx(). Прочитав очень внимательно описание к этой функции, становится понятно, что перед тем, как вызвать
GlobalMemoryStatusEx(), необходимо создать структуру
MEMORYSTATUSEX, в которую собствено и будет помещена вся необходимая информация.
typedef struct _MEMORYSTATUSEX {
DWORD dwLength;
DWORD dwMemoryLoad;
DWORDLONG ullTotalPhys;
DWORDLONG ullAvailPhys;
DWORDLONG ullTotalPageFile;
DWORDLONG ullAvailPageFile;
DWORDLONG ullTotalVirtual;
DWORDLONG ullAvailVirtual;
DWORDLONG ullAvailExtendedVirtual;
} MEMORYSTATUSEX, *LPMEMORYSTATUSEX;
Другими словами, мы должны создать требуемую этой функцией пустую структуру и только затем вызвать
GlobalMemoryStatusEx(), передав ей в качестве параметра адрес структуры. После чего можно смело читать интересующие нас данные из MEMORYSTATUSEX (см. поля
"ullTotalPhys" и
"ullAvailPhys"). Почему
GlobalMemoryStatusEx() сама не может создать необходимую ей структуру? Здесь ничего страшного нет. Почти все API функции работают по такому принципу, т.е. не выделяют и не освобождают никакой памяти, и всю эту работу перекладывают на вас. И это правильно. И последнее замечание. В AutoIt поддерживаются далеко не все типы данных, которые встречаются в MSDN, но в боьшинстве случаев того, что есть, вполне достаточно. В данном случае, в описании структуры MEMORYSTATUSEX присутствует тип DWORDLONG, которого нет в AutoIt. Но это не проблема,
DWORDLONG представляет собой 64-битное или 8-байтное беззнаковое число с диапазоном возможных значений от 0 до 18446744073709551615. Поэтому смело меняем этот тип на тот, который доступен в AutoIt, т.е. на
UINT64. В AutoIt нет никакой разницы между этими типами. Ниже представлен код для вызова функции
GlobalMemoryStatusEx() с небольшими комментариями.
; Создаем структуру MEMORYSTATUSEX
; http://msdn.microsoft.com/en-us/library/aa366770%28VS.85%29.aspx
$tMem = DllStructCreate( _
"dword dwLength;" & _
"dword dwMemoryLoad;" & _
"uint64 ullTotalPhys;" & _
"uint64 ullAvailPhys;" & _
"uint64 ullTotalPageFile;" & _
"uint64 ullAvailPageFile;" & _
"uint64 ullTotalVirtual;" & _
"uint64 ullAvailVirtual;" & _
"uint64 ullAvailExtendedVirtual;")
; До вызова Функции GlobalMemoryStatusEx() необходимо в поле "dwLength" записать размер структуры в байтах
; Получаем размер структуры $tMem
$iSize = DllStructGetSize($tMem)
; Записываем в поле "dwLength" значение из $iSize (размер структуры)
DllStructSetData($tMem, "dwLength", $iSize)
; Получаем адрес структуры $tMem
$pMem = DllStructGetPtr($tMem)
; Вызываем API функцию GlobalMemoryStatusEx(), которая заполнит необходимые поля в структуре $tMem
; http://msdn.microsoft.com/en-us/library/aa366589%28VS.85%29.aspx
DllCall("kernel32.dll", "bool", "GlobalMemoryStatusEx", "ptr", $pMem)
; Читаем значения из полей "ullTotalPhys" и "ullAvailPhys" (количество установленной и доступной памяти соответственно, в байтах)
$iTotalPhys = DllStructGetData($tMem, "ullTotalPhys")
$iAvailPhys = DllStructGetData($tMem, "ullAvailPhys")
; Освобождаем занимаемую структурой $tMem память (можно пропустить, т.к. программа на этом заканчивается)
$tMem = 0
; Выводим результат в консоль
ConsoleWrite("Всего памяти: " & StringFormat("%.2f МБ", $iTotalPhys / 1024 / 1024) & @CR)
ConsoleWrite("Доступно: " & StringFormat("%.2f МБ", $iAvailPhys / 1024 / 1024) & @CR)
Всего памяти: 2814.54 МБ
Доступно: 2005.82 МБ
API и ресурсы.
В процессе...
Содержание