So I've been wondering this for a while -- What is the difference between coinvoke and mcall... and why have 2 different macros to do the same thing (how many different ways are their to call a virtual function)? And since directx and example of COM how come I still don't understand this any better? ERNIE!!!! :) :)

And since Caleb uses coinvoke and Scronty uses mcall I was wondering how hard is it to convert one from the other?

using coinvoke (Caleb)


mov g_pD3D, eax
mov edi, eax

; *** Get infos about the default adapter ***

lea ecx, d3ddm [esp]
coinvoke edi, IDirect3D8, GetAdapterDisplayMode, \
D3DADAPTER_DEFAULT, ecx



or mcall (Scronty)



mov g_pD3D, eax

; Get the current desktop display mode
mcall [g_pD3D],IDirect3D8_GetAdapterDisplayMode, \
D3DADAPTER_DEFAULT,ADDR d3ddm

Posted on 2002-01-09 14:59:22 by Sliver
Hi sliver !

I had begun with the includes from Sergey Chaban and he uses mcall. After I had released my first attempts of DirectX8-Includes =CC=Ambush asked me if I could make it coincoke-compatibilty. At this time I never have known something about coinvoke (I'm not a COM-specialist at all). At first I thought it would be easy to make both compatible but after studing the way interfaces are defined by coinvoke, I gave that up. But I like the way methods are defined by the coinvoke-compatible macros and to me seems the way mcall is hacked suspecious (for example the org $-2 directive) so I decide to support coinvoke only.

This is how interfaces are declared for coinvoke-usage:



comethod1Proto typedef proto : DWORD
comethod1 typedef ptr comethod1Proto
.
:

_vtIUnknown MACRO CastName:REQ
; IUnknown methods
&CastName&_QueryInterface comethod3 ?
&CastName&_AddRef comethod1 ?
&CastName&_Release comethod1 ?
ENDM

IUnknown STRUCT
_vtIUnknown IUnknown
IUnknown ENDS


And this is the mcall version:



DECLARE_INTERFACE MACRO ifacename
ifacename typedef DWORD
CurrInterfaceName TEXTEQU <ifacename>
ifaceMethodVirtualOffset=0
ENDM


STDMETHOD MACRO MethodName:REQ,Args:VARARG
CurrMethodName CATSTR CurrInterfaceName,<MethodName>
CurrMethodProto CATSTR CurrMethodName,<Proto>
CurrMethodProto typedef PROTO Args
CurrMethodName = ifaceMethodVirtualOffset
;* Advance to the next position in vtable
ifaceMethodVirtualOffset=ifaceMethodVirtualOffset
+sizeof(LPVOID)
;;%ECHO &CurrMethodName
ENDM

; Define IUnknown interface, which is common for all objects
DECLARE_INTERFACE <IUnknown_>
; /*** IUnknown methods ***/
STDMETHOD <QueryInterface>,:PTR,:PTR PTR
STDMETHOD <AddRef>
STDMETHOD <Release>



So with the first version it's easier to make extended instances (I hope this is the right word !) of existing interfaces because they are fully implemented as macros

The way methods are called by coinvoke is



coinvoke MACRO pInterface:REQ, Interface:REQ, Function:REQ, args:VARARG
.
:
istatement CATSTR <invoke (Interface PTR[edx]).
&Interface>,<_>, <&Function, pInterface>
IFNB <args> ;; add the list of parameter if any
istatement CATSTR istatement, <, >, <&args>
ENDIF
mov edx, pInterface
mov edx, [edx]
istatement


and the one called by mcall is



mcall MACRO ObjPtr:REQ,MethodName:REQ,Args:VARARG
CurrMethodProto CATSTR <MethodName>,<Proto>
.
:
invoke CurrMethodProto ptr [eax],Args
ORG $-2
nop
nop
mov eax,ObjPtr
push eax ; this
mov eax,[eax] ; vtable*
call dword ptr [eax+MethodName]


The last lines are really a damned hack and not fine at all. Coinvoke instead is well declared which makes my decision easier.

The disadvantage of COINVOKE is the usage of the edx-register so you can't store local data in while calling a method. You can't change it to EAX because if you use byte or word parameters then they will be stored in AL / AX first and then pushed onto the stack. (Personally I prefer to load the object-ptr into EDI and then to call the interface-methods direct ...)

I don't know if this helps you answering your question ... but I hope for most people my decision is in friendly terms with.

Greetings, CALEB
Posted on 2002-01-09 16:15:28 by Caleb
Afternoon, Sliver.

Even though you're talking about DX examples, this is actually a COM related issue. heh - or even a Crusades topic.:grin:

heh. NaN's already queried about this:
http://www.asmcommunity.net/board/index.php?topic=1282&highlight=mcall

Except for the macros themselves, the main difference (for me) is that the mcall macro from Bizzare Creations is free - no licence necessary for building a commercial app.
Otherwise, Ernie's COM stuff is well documented, and used by more people here.

The *only* reason I use mcall, is because I started mucking about with win32asm DX from the Bizzare Creations' example.
At the time, there were about a dozen DX examples in assembly out there on the web(at least - ones that I knew about), and *each* one had their own way of implementing the calls.

Since I was just starting out with the win32asm stuff, I chose to use the code which was easiest(for me) to read. That ended up being the DX example of Sergeys'.

Now, as to converting from one to another:
heh. Are you talking about the calls, themselves,... or the include files?
As you can see from the code you posted, it'd be *very* easy to change from one call-type to the other. Just make sure you use the correct include files - they're slightly different.

Then, again. I'm sure the COM fellas will have something to say about that.:tongue:

Cheers,
Scronty
Posted on 2002-01-09 16:50:49 by Scronty
Afternoon, All.

This topic was originally posted in the Game Programming forum.

I've shifted it here so that the COM lurkers would be able to supply a more accurate picture of the pros/cons between the two calling methods.

I've also kept a link to this topic in the Game Programming forum - as any answers would be directly related to anyone interested in programming with DX.

Cheers,
Scronty
Posted on 2002-01-10 08:10:49 by Scronty
Wow, a lot has gone on here today I see.

Well, which is better? The one you prefer. It's like Coke or Pepsi.

coinvoke has the advantage of a kind of "interface inheritance" (that's the term Caleb was searching for), allowing interfaces to build on each other, for those who don't like typing "QueryInterface, AddRef, Release" over an over.

There are also some other subtle things going on with coinvoke, such as it decorates member method names (the procs) in a simple repeatable way (i.e., interface name, underscore, method name) so polymorphic methods (i.e., those with same names) will not conflict with each other.

A parameter count is also declared in the proto, so when ultimately the invoke macro is used the compiler does some checking for you to make sure you're not going to cause a stack crash.

coinvoke did not spring from some well of knowledge by itself. Bill T's original work in COM started the inspiration. Then I started WORKING in COM, both using COM to so some simple things, and the building CoLib itself.

The mcall came from roots I can only guess at, and my guess would be a developer needing an asm call to a COM interface, but not someone who cared for COM itself. So once he got something that worked, he kept it. It may not be pretty (my esthetic sense rebels at "ORG $-2") but when you get down to it, it works, it works well, and for some may even be more efficient then coinvoke, as it preserves more of the registers for other uses.

Additionally, the STDMETHOD macro in the interface definitions will allow a symbolic debugger to take your symbol list and actually keep on tracing code to the COM method. (At least I think so, people have bitched to me coinvoke/CoLib does not do this, and since I've never used a symbolic debugger I'm forced to agree).

So WHY will they both work? Let's begin with objects 101:

An object is a run-time allocated section of memory. When you get an object, you get the address of some portion of it. In most OOP methodologies, this address holds another address, that being the starting address of a table of methods.

Again, slowly, from the other direction. You define several procedures, then make a table to point to them. The table may look like so (for a COM thing):



FunctionPointers DWORD OFFSET QueryInterface
DWORD OFFSET AddRef
DWORD OFFSET Release
DWORD OFFSET MyFirstMethod
DWORD OFFSET MyOtherMethod


Then I'll declare this is the INTERFACE to my object. Someplace else in my code I'll call some object creation method. This method will so several things, first being to alloc some memory. The next thing it will do is place the address FunctionPointers into this alloced blob. Finally, the create method hands me back the address (call it pObject) of the alloced memory.

Knowing this should make clearer how and why COM (or OOP in general) methods are called. We have our object address, or pointer. If we Get the contents of this pointer (deference it), we get another pointer, that of the function table. So if we add the number of the function we wish to call (times 4 for dwords, of course), then get the contents (deference once more), we finally arrive at the address of the method we originally wanted to perform.

In code, these steps are simple:



mov eax, pObject ; get the address of the alloced object
; this pointer to the pointer to the function table
mov eax, [eax] ; now we have a pointer to the function table itself
call [eax, method_offset] ; do that method!


You add method_offset to eax to call each method, ie, add zero for QueryInterface, add 4 for AddRef, add 12 for MyFirstMethod, and so on.

They code lines may even look familiar, as both coinvoke and mcall do these three steps as a finally. OK, these macros are a bit more complex because they also allow PARAMETERS to be sent, but hopefully you see the point.

I must also note something else here, and that is the address of the object (or pObject here) is always passed to the method. It needs to be there so the object code knows WHICH object it is working with. Inside the object code, pObject is known as "this," as in "we are working with this object."


If you wish to write your own OOP implementation, you have a blank sheet of paper to use, and may arrange items more to your liking, such as always passing this in a register.

However, the rules are much stricter with COM, all parameters must be passed on the stack, because in COM you can't assume the object you are calling is residing in the calling process. It may even be on another machine. So to perform this "magic" transformation, COM asks you put your parameters in memory (on the stack) so it knows where to find them easily.


Thus endth the lesson.
Posted on 2002-01-10 22:28:04 by Ernie
If your not going to use the COM stuff, I would think it'd be best to strip the whole interface down to a single equate for each object and a macro. The equate would be a two dimentional array: each function would have a name and a count for the number of parameters passed to it. Could program in error checking and such into the macro. Of course, this defeats the whole purpose of COM, but the macro could host all sorts of warts and knobs that some programers seem to run screeming from. mcall lands somewhere in between and that is why I don't like it.
Posted on 2002-01-10 23:37:04 by bitRAKE