Вызов методов классов и Calling convention

Программирование на Visual Basic, главный форум. Обсуждение тем программирования на VB 1—6.
Даже если вы плохо разбираетесь в VB и программировании вообще — тут вам помогут. В разумных пределах, конечно.
Правила форума
Темы, в которых будет сначала написано «что нужно сделать», а затем просьба «помогите», будут закрыты.
Читайте требования к создаваемым темам.
Enemy314
Начинающий
Начинающий
 
Сообщения: 5
Зарегистрирован: 05.09.2008 (Пт) 18:19

Вызов методов классов и Calling convention

Сообщение Enemy314 » 05.09.2008 (Пт) 18:54

Здравствуйте, уважаемые специалисты!

... хотел написать предысторию, но стер, решив не тратить на бурду Ваше время...
сразу к делу.
использовав превосходную штуку от Хакера FireNativeDll, создал следующую прогу:
Код: Выделить всё
'cSimpleInterface.cls
'простенький интерфейс из одного метода
Option Explicit

'0 - QueryInterface
'1 - AddRef
'2 - Release
'3 - ?
'4 - ?
'5 - ?
'6 - ?

'7 - virtual int __stdcall Mult(int x, int y) = 0;
Public Function Mult(ByVal x As Long, ByVal y As Long) as Long 'простой метод: получить два числа, перемножить, вернуть результат умножения
End Function

Код: Выделить всё
'cSimpleClass.cls
'реализация простенького интерфейса
Option Explicit
Implements cSimpleInterface

'virtual int __stdcall Mult(int x, int y) = 0;
Public Function cSimpleInterface_Mult(ByVal x As Long, ByVal y As Long) as Long
  On Error Resume Next
  Dim Result As Long
  Result = x * y
  'MsgBox "x = " + CStr(x) + ", y = " + CStr(y) + "; Result = " + CStr(Result)    -- если раскомментарить, то отработает нормально и покажет правильный результат (см. ниже)
  cSimpleInterface_Mult = Result
End Function

Код: Выделить всё
'modLibrary.bas
'описание экспортируемых функций
Option Explicit

Public Declare Function GetMem4 Lib "msvbvm60" (ByVal pSrc As Long, ByVal pDst As Long) As Long
Public Declare Function PutMem4 Lib "msvbvm60" (ByVal pDst As Long, ByVal NewValue As Long) As Long

Private cls As cSimpleInterface

Private Const NCOMMethods As Integer = 7

Private Const DLL_PROCESS_ATTACH As Long = 1
Private Const DLL_PROCESS_DETACH As Long = 0
Private Const DLL_THREAD_ATTACH As Long = 2
Private Const DLL_THREAD_DETACH As Long = 3

Public Function DllEntryPoint(ByVal hInstance As Long, ByVal lReason As Long, ByVal lReserved As Long) As Long             
    Select Case lReason
        Case DLL_PROCESS_ATTACH
            DllEntryPoint = 1
        Case DLL_PROCESS_DETACH
        Case DLL_THREAD_ATTACH
        Case DLL_THREAD_DETACH
    End Select
End Function

'extern "C" void __stdcall GetClass (void ** p_class)
'выдает указатель на класс, имплементирующий простенький интерфейс, исключая из него методы СОМ-интерфейсов
Public Sub GetClass(ByRef p_class As Long)
  Dim pVTbl As Long
  Set cls = New cSimpleClass
  p_class = ObjPtr(cls) 'берем указатель на класс
 
  GetMem4 p_class, VarPtr(pVTbl) 'получаем указатель на таблицу виртуальных методов
  PutMem4 p_class, pVTbl + 4 * NCOMMethods 'смещаем указатель на количество методов, принадлежащих СОМ-интерфейсам, чтобы он указывал на таблицу только нашего простенького интерфейса
End Sub


ну и в CStdDLLInfo.cls -
.export "GetClass", AddressOf GetClass

всё это добро компилится в DLL и используется С++-ой прогой примерно следующего содержания:
Код: Выделить всё
//main.cpp
#include <windows.h>
#include <iostream>

using namespace std;

class ISimpleInterface //аналог простого интерфейса
{
public:
   virtual int __stdcall Mult(int x, int y) = 0;
};

extern "C" void (__stdcall *pGetClass) (void ** p_class) = NULL; //указатель на импортируемую функцию

void main()
{
   HMODULE hSrv = NULL;

   hSrv = ::LoadLibrary("dll_vb.dll");
   if(!hSrv)
   {
      cout << "Can't load dll\n";
   }
   else
   {
      pGetClass = reinterpret_cast<void (__stdcall *) (void ** p_class)>   (::GetProcAddress(hSrv,"GetClass"));   
      if(!pGetClass)
      {
         cout << "Can't get GetClass address\n";
      }
      else
      {
         cout << "GetClass address: " << pGetClass << "\n";
         ISimpleInterface * p_intr;
         (*pGetClass)((void **)&p_intr); //получаем указатель на объект, имплементирующий интерфейс
         cout << "GetClass() finished\n";
         cout << "p_intr = " << p_intr << "\n";
         int x = 4, y = 5, ret = 0;
         ret = p_intr->Mult(x,y); //пытаемся перемножить два числа
         cout << "p_intr->Mult(4,5) = " << ret << "\n";
      };
   };
}


метод Mult вызывается нормально, проверял по-всякому, да хотя бы впихнутый в него MsgBox - отрабатывает и результат умножения показывает корректно.

суть проблемы: метод Mult возвращает своё Long-овое значение не понимаю как (в ret записывается ноль всегда)... Ведь каждый класс в VB - СОМ-объект? а у методов СОМ-объектов stdcall? а в stdcall 4-байтовое целое возвращается в eax? судя по дизассемблированному коду, который показывает студия, прога на С++ так и ожидает получить результат - после вызова метода делает mov dword ptr [ret], eax. Но судя по тому же дизассемблированию, результат функции Mult кладется непойми куда (в какой-то адрес памяти, но не в стек и не в регистр, насколько мне позволяют судить скромные познания асма).

Бился сегодня весь день, честно пытался докопаться до решения самостоятельно через форумы, МСДН, гугль... узнал за день больше чем за всё лето, но как решить проблему так и не понял, более того - даже не понял в чем проблема, почему так выходит, ведь должно быть по-другому...

помогите, пожалуйста, люди добрые, если не решить проблему, то хотя бы понять в чем она заключается)) заранее искренне благодарен всем откликнувшимся!

п.с. кстати, обычные функции (не методы класса) в VB-шных DLL-ях прекрасно возвращают Long-овые значения. В eax.



UPD.
решил все-таки добавить немного дизассемблированного кода, может быть это важно...
Код: Выделить всё
;         ret = p_intr->Mult(x,y);
; вызов метода Mult по самому первому адресу из VTbl
0040403E  mov         eax,dword ptr [y]
00404044  push        eax 
00404045  mov         ecx,dword ptr [x]
0040404B  push        ecx 
0040404C  mov         edx,dword ptr [p_intr]
00404052  mov         eax,dword ptr [edx]
00404054  mov         ecx,dword ptr [p_intr]
0040405A  push        ecx 
0040405B  mov         edx,dword ptr [eax]
0040405E  call        edx 
00404060  mov         dword ptr [ret],eax ; вот тута ждут результат, если я правильно понял

<...>

; собственно, что происходит после call-a...

; здесь быломного совершенно непонятного для меня кода, наверное связанного с инициализацией переменных; я его вырезал

; хотя назначение дальнейшего кода мне тоже непонятна, за исключением нескольких строчек...
11004342  mov         edx,dword ptr [ebp+0Ch]  ; вот тут начинаются вычисления; [ebp+0Ch] - это адрес ByVal x As Long
11004345  imul        edx,dword ptr [ebp+10h]  ; [ebp+10h] - адрес ByVal y As Long
11004349  jo          1100438C
1100434B  mov         dword ptr [ebp-24h],edx ; предполагаю, что [ebp-24h] - это адрес Dim Result As Long
1100434E  mov         dword ptr [ebp-4],4
11004355  mov         eax,dword ptr [ebp-24h]
11004358  mov         dword ptr [ebp-28h],eax
1100435B  mov         dword ptr [ebp-10h],0 ; вот он, этот нолик, который ниже перерастет в eax и в итоге запишется в переменную int ret
11004362  mov         ecx,dword ptr [ebp+8]
11004365  mov         edx,dword ptr [ecx]
11004367  mov         eax,dword ptr [ebp+8]
1100436A  push        eax 
1100436B  call        dword ptr [edx+8]
1100436E  mov         ecx,dword ptr [ebp+14h]
11004371  mov         edx,dword ptr [ebp-28h]
11004374  mov         dword ptr [ecx],edx  ; ! а вот тут ! иногда (закономерность установить не смог) вылетает Access violation; а иногда не вылетает...
11004376  mov         eax,dword ptr [ebp-10h] ; записывается нолик
11004379  mov         ecx,dword ptr [ebp-20h]
1100437C  mov         dword ptr fs:[0],ecx
11004383  pop         edi 
11004384  pop         esi 
11004385  pop         ebx 
11004386  mov         esp,ebp
11004388  pop         ebp 
11004389  ret         10h 
Последний раз редактировалось Enemy314 05.09.2008 (Пт) 19:58, всего редактировалось 1 раз.

ANDLL
Великий гастроном
Великий гастроном
Аватара пользователя
 
Сообщения: 3450
Зарегистрирован: 29.06.2003 (Вс) 18:55

Re: Вызов методов классов и Calling convention

Сообщение ANDLL » 05.09.2008 (Пт) 21:02

а у методов СОМ-объектов stdcall?
stdcall
Но COM-функции возвращают через eax код ошибки(или признак завершения, в частности 0 означает "все ок"). А результат вовзращается через последний параметр
Гастрономия - наука о пище, о ее приготовлении, употреблении, переварении и испражнении.
Блог

Enemy314
Начинающий
Начинающий
 
Сообщения: 5
Зарегистрирован: 05.09.2008 (Пт) 18:19

Re: Вызов методов классов и Calling convention

Сообщение Enemy314 » 05.09.2008 (Пт) 21:14

спасибо за быстрый ответ!

ANDLL писал(а):
а у методов СОМ-объектов stdcall?
stdcall
Но COM-функции возвращают через eax код ошибки(или признак завершения, в частности 0 означает "все ок"). А результат вовзращается через последний параметр

хм, вот много чего за сегодня перечитал (в частности, RSDN), но не встречал чтобы это условие входило в "стандарт" СОМ... как я понял, лишь необязательная условность...
или это именно особенность VB? нда, третий параметр всё объясняет, и периодические Access violation тоже...
а скажите, пожалуйста, как передается этот последний параметр, содержащий результат? и как формируется код ошибки (могу ли я на него повлиять из VB)? или где бы об ентом почитать?, чесслово не встречал подробного описания работы компилятора VB...

upd. Err.Raise меняет код возврата, но как-то пока неконтролируемо, какой бы аргумент ни дал - С++ ловит одно и то же число, -2146828283 (Warning
| FACILITY_CONTROL | ERROR_ACCESS_DENIED , если я еще помню как переводить чиселки)...

ANDLL
Великий гастроном
Великий гастроном
Аватара пользователя
 
Сообщения: 3450
Зарегистрирован: 29.06.2003 (Вс) 18:55

Re: Вызов методов классов и Calling convention

Сообщение ANDLL » 05.09.2008 (Пт) 22:19

хм, вот много чего за сегодня перечитал (в частности, RSDN), но не встречал чтобы это условие входило в "стандарт" СОМ... как я понял, лишь необязательная условность...
Каг бе стардарт COM никак не ограничивает в теории даже callling conversion у функций в интерфейсе.
Но на практике функции COM должны быть stdcall и возвращать именно HRESULT, если ты хочешь что бы твой интерфейс мог использовать ктото кроме тебя на языке отличном от c++.
а скажите, пожалуйста, как передается этот последний параметр, содержащий результат?
Как обычно.
и как формируется код ошибки (могу ли я на него повлиять из VB)?
Когда err.raise получает отрицательное число, то именно это число оказывается в коде возврата
Гастрономия - наука о пище, о ее приготовлении, употреблении, переварении и испражнении.
Блог

Enemy314
Начинающий
Начинающий
 
Сообщения: 5
Зарегистрирован: 05.09.2008 (Пт) 18:19

Re: Вызов методов классов и Calling convention

Сообщение Enemy314 » 05.09.2008 (Пт) 22:39

ANDLL писал(а):
а скажите, пожалуйста, как передается этот последний параметр, содержащий результат?
Как обычно.

ээ, а как - "обычно"? "... , ByRef Result As Long" ( "..., int * result") в моём случае? я с VB-то пока на Вы, для мну тут всё - "необычно", или скорее "непривычно", сначала долго удивлялся что методов у класса больше чем объявлено, теперь вот результат функции в параметрах - непривычно...

ANDLL
Великий гастроном
Великий гастроном
Аватара пользователя
 
Сообщения: 3450
Зарегистрирован: 29.06.2003 (Вс) 18:55

Re: Вызов методов классов и Calling convention

Сообщение ANDLL » 06.09.2008 (Сб) 9:07

Enemy314 писал(а):ээ, а как - "обычно"? "... , ByRef Result As Long" ( "..., int * result")
Ага.
Это вобщемто распространненный прием в программировании. Куча Windows API функций устроены подобным образом.
Гастрономия - наука о пище, о ее приготовлении, употреблении, переварении и испражнении.
Блог

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16478
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Вызов методов классов и Calling convention

Сообщение Хакер » 06.09.2008 (Сб) 9:42

использовав превосходную штуку от Хакера FireNativeDll,

Надо же, хоть кому-то это оказалось нужным и полезным.

По поводу проекта:
Мне представляется, что было бы гораздо красивее объявить нужные мемберы в сишном интерфейсе, а функцию

Код: Выделить всё
Public Sub GetClass(ByRef p_class As Long)
  Dim pVTbl As Long
  Set cls = New cSimpleClass
  p_class = ObjPtr(cls) 'берем указатель на класс
 
  GetMem4 p_class, VarPtr(pVTbl) 'получаем указатель на таблицу виртуальных методов
  PutMem4 p_class, pVTbl + 4 * NCOMMethods 'смещаем указатель на количество методов, принадлежащих СОМ-интерфейсам, чтобы он указывал на таблицу только нашего простенького интерфейса
End Sub


упростить до

Код: Выделить всё
Public Sub GetClass(ByRef p_class As cSimpleClass)
  Set p_class = New cSimpleClass
End Sub



Тогда можно было бы заодно вообще отказаться от созданеия функции GetClass и вызывать DllGetClassObject, получая интерфейсный указатель оттуда.

Если же всё-таки использовать функцию GetClass, то рекомендую выставить классу свойство инстансинг в Private.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

Enemy314
Начинающий
Начинающий
 
Сообщения: 5
Зарегистрирован: 05.09.2008 (Пт) 18:19

Re: Вызов методов классов и Calling convention

Сообщение Enemy314 » 06.09.2008 (Сб) 12:40

ANDLL писал(а):
Enemy314 писал(а):ээ, а как - "обычно"? "... , ByRef Result As Long" ( "..., int * result")
Ага.
Это вобщемто распространненный прием в программировании. Куча Windows API функций устроены подобным образом.

угу, просто первый раз столкнулся с "перескакиванием" возвращаемого результата в список параметров...
спасибо за разъяснения!

Хакер писал(а):Надо же, хоть кому-то это оказалось нужным и полезным.

по-моему, очень полезная штука)) иметь возможность, например, в с++-проектах кусочки GUI легко и просто писать на VB (вместо гемора с MFC) - это приятно)) спасибо за этот продукт)


Хакер писал(а):Мне представляется, что было бы гораздо красивее объявить нужные мемберы в сишном интерфейсе, а функцию

да, пожалуй это было бы и правильнее; а смещая pVTbl я, образно выражаясь, "избавляюсь от хорошего и правильного".
конечно, если есть продуманный СОМ то изобретать велосипед - неэтично. но в данном случае у меня, признаться, был чисто теоретический интерес, в котором С++-ый интерфейс следовало воспринимать как данность (считая, что клиентский ехе-шник уже написан, и надо теперь написать dll-сервер на VB).


Хакер писал(а):Если же всё-таки использовать функцию GetClass, то рекомендую выставить классу свойство инстансинг в Private.

хммм, я про это пока ничего не знаю, а с чем это связано?
пойду поищу что-нибудь почитать про эти свойства VB-классов которые можно выставлять...

и, кстати, может это как-то связано с тем, что я как раз хотел спросить. когда VB вызывает методы QueryInterface, AddRef и Release? как сделать так, чтобы после завершения GetClass сам VB уже "забыл" про созданный объект (считал, что ни одна переменная на него не ссылается), но он бы остался в памяти, причем со счетчиком ссылок = 1?
предполагаю, что после строки p_class = ObjPtr(cls) нужно "вручную" вызвать cls.AddRef (по адресу из VTbl), а потом сделать Set cls = Nothing; этого достаточно?

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16478
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Вызов методов классов и Calling convention

Сообщение Хакер » 06.09.2008 (Сб) 13:16

Enemy314 писал(а):а с чем это связано?

С тем, будет ли класс создаваемым извне, или его можно будет создавать только внутри программы. Как следствие, класс будет помещён в TLB не как COCLASS, а как INTERFACE. Если публичных классов нет, то DLL также не будет иметь 4 экспортируемых функции DllGetClassObject, DllRegisterServer, DllUnregisterServer, DllCanUnloadNow (это следствие — фича самого FNDLL, а не VB).


когда VB вызывает методы QueryInterface, AddRef и Release?

  1. Когда производится type-convertion из одного объектного типа в другой. А происходит это при присвоении одной переменной-ссылке значения другой переменной-ссылки, или же при передачи значения переменной-ссылки в качестве аргумента-ссылки, тип которой отличается от типа первой.
  2. Вызывается из оператора присваивания ( = ) для правого операнда, если он на что-то ссылается (т.е. если указатель ненулевой). Вызывается также при передаче одной переменной-ссылки в качестве аргумента в процедуру.
  3. Вызывается из оператора присваивания ( = ) для левого операнда, если он на что-то ссылается (до того, как будет произведено присвоение, разумеется). Вызывается также внутри любой процедуры для всех её ненулевых объектных переменных и аргументов.

как сделать так, чтобы после завершения GetClass сам VB уже "забыл" про созданный объект

Никак, потому что VB и так не помнит ничего ни про один созданный объект. Помнят исключетельно сами объекты о том, что на них ссылаются из N-мест. Когда количество ссылок снижается (вызовами IUnknown::Release) до нуля, объект (вернее код внутри IUnknown::Release) удаляет себя. Сам по себе VB в этом абсолютно никак не участвует. Поэтому работать как в VB с неVB-шными объектами, так и наоборот, из другого языка в VB-шным объектом. Объект попросту никак не специфичен для "языка", "язык" ничего не знает об объекте; объект просто соответствует стандартам COM.


предполагаю, что после строки p_class = ObjPtr(cls) нужно "вручную" вызвать cls.AddRef (по адресу из VTbl)


Предполагаешь в принципе правильно. Только вызывать нужно не по адресу из VTable, а вызывать нормально т.е. средствами языка, скастовав ссылку cls к типу IUnknown. В VB кастования отсутсвует (что, к сожалению, делает использование интерфейсом очень неудобным), так что скастовать можно только через доп. переменную. Чтобы стал доступен интерфейс IUnknown и возможность кастования к нему, надо чтобы он был объявлен. Объявлять интерфейсы полноправно нельзя (т.е. нельзя, например, указать, какой должен-быть IID интерфейса), поэтому IUnknown должен быть объявлен в TLB которую нужно подключить. А TLB можно либо сделать самому, либо взять olelib от Edanmo.

В исходниках fndll всюду всё вызывалось именно "по адресу из VTbl". Я решил сделать так потому, что эданмовская TLB имела какие-то косяки, а показать всем, как можно вызывать методы по указателю было полезно.

Кстати, прочитав этот вопрос, мне стало казаться, что под вопросом "как заставить VB забыть про созданный объект" имелась ввиду именно глобальная ссылка cls. Заставить забыть очень просто: убрать глобальную ссылку cls и воспользоваться локальной переменной, делая таки AddRef при вызове.

Set cls = Nothing; этого достаточно?

Для глобальной переменной — да. Но тебе не нужна глобальная переменная в таком случае (зачем глобальная переменная, если она используется только один раз и только в пределах одной функции?), а нужна локальная. Для локальной Set ... = Nothing делать не нужно, VB это в любом случае сделает сам при выходе из процедуры.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

Enemy314
Начинающий
Начинающий
 
Сообщения: 5
Зарегистрирован: 05.09.2008 (Пт) 18:19

Re: Вызов методов классов и Calling convention

Сообщение Enemy314 » 06.09.2008 (Сб) 13:36

Хакер, благодарю за столь подробный ответ!
cls я сделал глобальной потому что интуиция (или какие-то давно прочитанные и тогда еще непонятые где-то посты?) мне подсказывала, что если сделать ее локальной и не вызывать AddRef, то VB, вызвав Release перед выходом из подпрограммы, заставит cls самоуничтожиться (и из вашего ответа это следует), и С++ достанется битая ссылка; а точнее даже у VB это сделать не получится - pVTbl ведь изменилась и по третьей ссылке непойми что вместо указателя на Release.


ок, буду разбираться с TLB, благо инфы, вроде, достаточно))

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16478
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Вызов методов классов и Calling convention

Сообщение Хакер » 06.09.2008 (Сб) 13:57

Enemy314 писал(а):cls я сделал глобальной потому что интуиция (или какие-то давно прочитанные и тогда еще непонятые где-то посты?) мне подсказывала, что если сделать ее локальной и не вызывать AddRef, то VB, вызвав Release перед выходом из подпрограммы, заставит cls самоуничтожиться (и из вашего ответа это следует), и С++ достанется битая ссылка;

Всё верно.

а точнее даже у VB это сделать не получится - pVTbl ведь изменилась и по третьей ссылке непойми что вместо указателя на Release.

Не понял. Что не получится у VB? — У VB всё получится :)

Единственное, что действительно при использовании локальной переменной возникнет определённая трудность. Дело в том, что VB чуть более умён, чем кажется: и Release делается делается для каждой переменной тупо в конце процедуры, а в конце её UsageContext-а. UsageContext заканчиваетяс у переменной в том месте, где она в последний раз используется.

Код: Выделить всё
Dim x as Something
Set x = New Something                    ' объект, созданный ЗДЕСЬ ...

ptr = ObjPtr(x)
                                                        ' будет сразу же уничтожен ТУТ
ptr = ptr + 123

SomeExternalPointerToObject = ptr



Решается это легко и само собой: UsageContext-нужно растянуть до вызова AddRef. Растянуть его таким образом легко — достаточно поюзать переменной где-ниубдь после вызова AddRef, например так:

Код: Выделить всё
Dim x as Something
Set x = New Something                    ' объект, созданный ЗДЕСЬ ...

ptr = ObjPtr(x)
                                                       
ptr = ptr + 123

SomeFunctionThatCallAddref()
ObjPtr x                                         ' гарантировано доживёт ДОСЮДА.

SomeExternalPointerToObject = ptr


Если же вызывать AddRef средствами языка, при этом в TLB описывая не IUnknown, а ISimpleInterface — вообще не нужно заморачиваться, т.к. вызов вида x.AddRef сам по себе растягивает UsageContext тем фактом, что при нём используется переменная AddRef.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.


Вернуться в Visual Basic 1–6

Кто сейчас на конференции

Сейчас этот форум просматривают: AhrefsBot и гости: 77

    TopList