Главная » Вызов функций по указателю
Однако есть функции для почти прямой работы с ними – частично скрытые (VarPtr, ObjPtr, StrPtr) и скрытые посильнее (см. статью про GetMem и PutMem). А вот вызова функций по указателю нет вообще, в то время как это мощнейший инструмент, очень удобный и простой. И привнести его в VB – деяние приятное и полезное. Многое из описанного в этой статье является весьма и весьма спорным с точки зрения переносимости, может, непереносимым вообще. Но возгласы "эй, а это будет работать в следующей версии VB?" не имеют силы: VB 7 уже вышел. VB 7 – это совсем не VB 6, это другой язык. Так что мы знаем, что VB 6 останется именно таким, какой он есть сейчас. И поэтому мы можем использовать даже самые непереносимые и несовместимые методики. При написании этой статьи я придерживался определённой хронологии и описывал решения в том порядке, в котором к ним приходил. Копирайт: сама идея вызова через CallWindowProc с использованием некоей ассемблерной вставки принадлежит не мне. Но реализация именно в таких вот видах и именно с такими вставками – моя. Взгляд изнутри Прежде всего, необходимо разобраться, что такое вызов функции на самом низком уровне, с точки зрения процессора. Как известно, у процессора есть набор инструкций, заложенных в него проектировщиками, которые он и исполняет. "Вызов" (функции) – это тоже просто одна из команд процессора. Их на самом деле несколько, но мы не будет углубляться в изучение ассемблера (во-первых, это выходит за рамки статьи, а во-вторых, я сам в этом не очень силён . Запомним пока, что есть такая команда. Теперь стек... Что такое стек? Это часть памяти (обыкновенной, оперативной), которая резервируется при запуске программы (нужный размер этой стековой памяти записывается компилятором прямо в exe при создании оного). Особенность работы со стеком описывается буквосочетанием LIFO (Last In, First Out; последним пришёл, первым вышел). Образно стек можно представить как трубу с одним открытым концом (а другой запаян). Можно положить что-то внутрь (при этом оно окажется на самом верху), если есть место, конечно. Можно убрать что-то (только самое верхнее, снизу нельзя). Можно посмотреть, что уже наличествует (смотреть можно и сверху, и глубже – представьте, что вся труба стеклянная). Принцип стека используется в программах (во всех программах) по двум причинам. Во-первых, память резервируется сразу при запуске программы, а значит, не будет расходов на её выделение в процессе работы (это обеспечивает высокую скорость). Во-вторых, это самый простой и удобный способ сохранения предыдущего состояния, чтобы сделать что-то другое и потом вернуться. Когда что-то помещается в верхушку стека, всё остальное остаётся неизменным, и потому после удаления верхнего элемента всё как-то само собой возвращается в исходное состояние, совершенно без лишних движений. Думаю, теперь можно переходить к описанию того, как на самом деле происходит вызов функции. Вызов функции – это помещение параметров в стек и выполнение процессорной команды call. Вот и всё. Существует несколько соглашений вызова (они определяют, как именно помещаются параметры в стек). Мы будем рассматривать только одно соглашение – StdCall. Во-первых, именно его используют все функции Windows, во-вторых, именно его использует VB. В соответствии с этим соглашением, параметры помещаются в стек в обратном порядке (справа налево), а функция сама удаляет их из стека по завершении работы. Допустим, у нас есть функция Function Sum (ByVal p1 As Long, ByVal p2 As Long, ByVal p3 As Long) As Long
которая возвращает сумму своих аргументов. Когда мы вызываем её, выполняется такой код: push p3
push p2
push p1
после чего происходит вызов функции Sum.
Как вы догадались, команда процессора push означает "поместить в верхушку стека". Как нетрудно видеть, первый аргумент окажется в самой верхушке стека, ведь он помещён туда последним. Функция Sum всё это знает, и свои аргументы оттуда берёт (она пока не удаляет их! она пользуется тем, что труба стеклянная). Закончив работу, функция Sum должна вернуть управление туда, откуда её вызвали (иначе программа остановится… вернее, она рухнет, но это детали…). Но КУДА должна вернуть управление функция Sum?
Я написал "после чего происходит вызов функции Sum", но я не написал, как именно он происходит. Очень просто - выполняется единственная команда процессора Call. Эта команда передаёт управление функции Sum, но перед этим помещает в верхушку стека адрес, по которому должна вернуться эта самая Sum! В результате непосредственно перед передачей управления в функцию Sum стек приобретает вид: Адрес возврата
Параметр p1
Параметр p2
Параметр p3
[всё, что глубже, помещено не нами и нас не интересует...]
Закончив работу, функция Sum вернёт управление по тому адресу, который лежит в самой верхушке стека. Непосредственно перед этим она удалит (на сей раз именно удалит, а не просмотрит) из стека и этот адрес, и все свои параметры.
Ну вот, теперь, кажется, мы знаем достаточно
Ну что, поехали… Напрямую...А зачем нам это всё? Ну писали бы на ассемблере, а из VB-то мы не можем вызвать конкретные команды процессора... Да, не могли бы, если бы не одна единственная API-функция CallWindowProc (собственно, ей нужно ставить памятник, она действительно ОДНА...). Эта функция предназначена для вызова обработчика оконных сообщений, но суть её работы сводится к простой упаковке параметров в стек и передаче управления! Иными словами, она не проверяет, что именно её заставляют вызвать (да и не смогла бы проверить при всём желании). А раз она обычная API-функция, то мы можем её Declare. Ну и всё, можете вызывать что хотите, спасибо за внимание...
Нет, на самом деле проблемы только начинаются.
Проблема первая – количество параметров. Нетрудно видеть, что у CallWindowProc параметров ровно пять, из них один – адрес функции (или указатель на функцию; что-то давно не упоминалось мною это славное слово). Значит, вызываемая функция должна иметь ровно 4 параметра. Ведь у нас соглашение StdCall, помните? А оно требует, чтобы функция удаляла свои параметры из стека сама. Если функция была откомпилирована для работы с 3 параметрами, она удалит из стека ровно 3 параметра, и вы даже не можете себе представить, насколько ей безразлично, сколько их было туда помещено на самом деле. Результат – нарушение структуры стека (вызывающая сторона в недоумении, она-то уверена, что удалены 4 параметра, а тут...) и немедленный crash. Так что использовать функцию CallWindowProc напрямую можно лишь в одном случае: если вы уверены, что вызываемый код завершается командой процессора ret 0x0010 (это команда "возврат" с удалением из стека &H10 байт. &H10 – это 16, а 16 – это 4*4, то есть 4 параметра по 4 байта каждый. Они все 4 байта, параметры-то). В этом можно быть уверенным у в двух случаях: вы знаете, что у функции 4 параметра или же вы сами написали некий код, завершающийся командой ret 0x0010. Чувствуете, куда клоню? wink
Даже если чувствуете, всё равно запомним промежуточный результат (промежуточных результатов будет несколько, и каждый имеет полное право на самостоятельное и независимое существование и использование):
Можно напрямую использовать функцию CallWindowProc для вызова другой функции. Для этого в качестве первого параметра нужно передать указатель на эту функцию, а в качестве остальных – параметры этой, вызываемой, функции. Жёсткое ограничение: у вызываемой функции должно быть ровно 4 параметра.
Приведём маленький код в подтверждение сказанного. Option Explicit

Private Declare Function CallWindowProc Lib "user32.dll" _
Alias "CallWindowProcA" (ByVal lpPrevWndFunc _
As Long, ByVal hwnd _
As Long, ByVal msg As Long, ByVal wParam _
As Long, ByVal lParam As _
Long) As Long
Private Declare Function _
FreeLibrary Lib "kernel32.dll" (ByVal _
hLibModule As Long) As Long
Private Declare Function GetProcAddress Lib "kernel32.dll"_
(ByVal _
hModule As Long, ByVal lpProcName As String) As Long
Private Declare Function LoadLibrary Lib "kernel32.dll" _
Alias "LoadLibraryA" (ByVal _
lpLibFileName As String) As Long

Private Sub Form_Load()
Dim user As Long

user = LoadLibrary("user32.dll")
CallWindowProc GetProcAddress(user, "MessageBoxA"), _
Me.hwnd, StrPtr(StrConv("Ну что, работает!", vbFromUnicode)), _
StrPtr(StrConv("Заголовок", vbFromUnicode)), 0
FreeLibrary user
End Sub
Пока не обращайте внимания на StrPtr и StrConv, потом всё скажу Категория: Языки программирования | Просмотров: 106