logo Sabbasoft
 

logo VB2TheMax

logo Code Architects

Welcome to Sabbasoft

 

VB-COM FAQ

 

1)

 

What's the difference between early binding and late binding ?

2)   Createobject vs New.  Dim as Object vs Dim as IMyInterface
3)   Why shouldn't I rely on VB runtime regarding objects release?
4)   Basic IDL syntax and mapping to VB class-module settings
5)   A few words about COM threading models
6)   How do I overcome VB's limits of the binary compatibility settings ?
7)   A few words about Byref / ByVal - Standard marshaling / Custom Marshaling By Value
8)   How do I implement proper Error Handling in VB ?
9)   Can I store Interface pointers in the SPM ?
10)   Binary compatibility of components
11)   Object pooling
 

 

1) What's the difference between early binding and late binding ?

Interfaces are the basic concept of COM. An interface is a group of methods definition (read signature) conceptually related. In COM this group of methods definition gets mapped at the binary level into a standard (among C++ compilers) layout (the so called VTable function pointer). 
Remember that interfaces describe methods signatures, not the implementation of them.
Interfaces are implemented by COM objects. 
A COM object implements one or more interfaces (the IUnknow interface is the only mandatory interface that each COM object must implement). 
An interface can be implemented by more than one COM object.
There is one "particular" (read "abomination") interface called the Dispatch interface (This interface is particular for the role it plays not for its binary layout). The Dispatch interface let's you define a so called Dispatch-based interface (aka Dispinterface). A Dispinterface is a logically related group of methods that are not mapped into to the corresponding binary layout. In the case of Dispinterfaces, method invocation pass through the Dispatch interface (specifically its Invoke method) that acts as a kind of gateway for the invocation of your Dispinterface methods. The method signature of the Invoke method is defined to accomodate "most" of method signature and data types. 
The set of the data-types available in Dispinterfaces is a subset of the data types available in COM. This subset is called automation compatible data-types. The fact you have to deal with a subset is not due directly to a limitation of the Invoke method. This limitation comes from the fact that Dispinterfaces can use only the automation marshaler when they have to marshall datas across apartments and processes. Standard interfaces can use they own marshaler that can handle a wider set of data types. 

When you bind to a "standard" interface you are early-binding to the interface. When you bind to a Dispinterface you are late-binding to the (Disp)interface.

Of course, early-binding out-performs late-binding cause the last one involves the overhaed of packing and unpacking datas back and forward to fit them into the Invoke method.
When you early-bind to an interface the compiler can check at build-time that methods of the interface are invoked correctly (method name, data types ..etc)
When you bind to a Dispinterface (that is when you late-bound) these checks can be done only at run-time.
What about VB ? 
When you (blindly :) ) start typing methods in the Class1 module VB:
- silently creates a _Class1 interface  and accomodate the methods signature into it. 
- Define Class1 has implementing the _Class1 interface.
Additionally VB defines _Class1 as a Dual interface, that is, it sets up a standard _Class1 interface AND a Dispinterface counter-part of the _Class1 interface, so that you can access your methods via early-binding AND late-binding. 
Note that VB marks this interface as the default interface of the object (remember you can define additional interfaces in your class via the Implements keyword). 
The fact that VB doesn't let you define "pure interfaces", that is, interfaces without their Dispinterface counterpart, limit VB developer to ole automation data types, even if they early-bound to the interface.

2) Createobject vs New.  Dim as Object vs Dim as IMyInterface

 

Consider this line of code:

Dim x as Project1.Class1

Most of us use/used to think that we were declaring x as a "Project1.Class1" class.
This is wrong. In this line of code x is declared as the _Class1 interface defined in the Project1 Type library (the underscore is hidden by the kindness of the VB team ;) ). Note that in the above line of code you have not made any assumption regarding what object will provide an implementation of  the Project1.Class1 interface.
It happens you write then:

Set x = CreateObject("Project1.Class1") 'OR
Set x = New Project1.Class1 'OR

One thing to stress: these 2 lines of codes are totally equivalent regarding early or late binding. There is a common misunderstanding among VB developers. They think that if you use the New keyword you are early-binding and that if you use CreateObject you are late-binding. 
This is uncorrect. if you want to late-bind to the Project1.Class1 interface (that is use its Dispinterface counterpart) you should have written:

Dim x as Object  ' read Object as Dispatch

The confusion arises from the fact that VB hides you from most of the details of interface and classes definition. 
- In the line [Dim x as Project1.Class1] Project1.Class1 is an interface indentifier (IID)
- In the line [Set x = New Project1.Class1] Project1.Class1 is a class identifier (The class CLSID)
- In the line [Set x = CreateObject("Project1.Class1")] "Project1.Class1" is the ProgId of the class, the ProgId is the human-readable name of the class that is mapped to the CLSID. This mapping is stored in the registry when you register your COM component (e.g. running regsvr32.exe if the component is a dll). 
If you create an object via the New keyword the CLSID is read from the referenced component type library at build time and hardcoded  in your component. If you use CreateObject VB queries the registry at run-time the map the ProgId to the CLSID (The CLSID is what you have to pass to the COM run-time when you ask it to create an object).
As you may know you can even write:

Dim x as New Project1.Class1

In this case [Project1.Class1] is a class identifier. You are asking VB to create the Project1.Class1 class, ask to the object its default interface and place it into the x variable.

 

3) Why shouldn't I rely on VB runtime regarding objects release?

  VB takes care to release the memory used by any variable when it goes out of scope, nevertheless it's better to Set explicitely your objects to Nothing for the following reasons:
- It's a good programming techinique, this habitude could turn usefull if it happens you to program in  C++.
- It's better to release resources when you don't need them anymore, without waiting they go out of scope, this is expecially true if the objects keep DB connections open.
- There are (yet ?) some ADO bugs that gives some problems (loss of information in the Err object) if you don't have set ADO objects explicitely to Nothing before raising an error.
- Close Recordsets and Connections objects before setting them to Nothing, or connection pooling won't work correctly.

4) Basic IDL syntax and mapping to VB class-module settings

  Although COM is "just" a binary layout definition, IDL has become the "C-like" lingua franca of COM interface and class definition. IDL is processed by MIDL.EXE to produce marshaling code (necessary for non automation compliant interfaces), header definition (for C++) and type library (for VB).


Here is an IDL example:

import "oaidl.idl";
import "ocidl.idl";
[
 object,
 uuid(D308366D-9185-11D3-9B0E-0000E8C951D0),
 dual,
 helpstring("IMYClass Interface"),
 pointer_default(unique)
]
interface IMYClass : IDispatch
{
 [id(1), helpstring("method MyMethod")] 
 HRESULT MyMethod(
 [in] BSTR MYString, [in,out] long* MyLong,[out,retval] VARIANT_BOOL* MyRetCode
 );
};

[
 uuid(D3083661-9185-11D3-9B0E-0000E8C951D0),
 version(1.0),
 helpstring("MyCOMServer 1.0 Type Library")
]
library MYCOMSERVERLib
{
 importlib("stdole32.tlb");
 importlib("stdole2.tlb");

[
 uuid(D308366E-9185-11D3-9B0E-0000E8C951D0),
 helpstring("MYClass Class")
]
coclass MYClass
{
 [default] interface IMYClass;
};
};


Note that only what is inside the library block (or is referenced in the library block) is brought into the type-library.
In this sample the library decribes a COM Object named MYClass ([coclass]) that implements the IMYClass interface. In VB you map such "standard" COM definition declaring a Class module as Multiuse.
PrivateNotCreatable classes are mapped to IDL tagging the [coclass] with the NonCreatable keyword. To get a pointer to such interfaces there must be another Multiuse in the same component (so that it knows how to create the COM object) that creates the PublicNotCreatable object, asks for one of its interface and passes it out of the component via a method parameter
. When you mark a class as Public not Creatable VB do more then this: it removes the InprocServer entry in the registry under the class CLSID; in this way the COM runtime has no way to create the class, even from languages like C++ that ignores the coclass section of IDL. 
I just recently realized that the whole coclass section are just for documentation purpouses and for VB clients.
Not creatable is an example, another example is the source tag to let VB know what is the default (actually the only VB can access) sink interface of the object (the default sink interface contains the events you define for the class in VB).
Private classes does not appear in IDL at all.


GlobalMultiuse classes: such classes have the appobject tag in the [coclass] definition. Such tag is just for VB use. A GlobalMultiuse class is a class whose methods you can invoke directly without creating the object explicitely. You can do this cause VB silently creates the object on your behalf the first time you invoke a method of such class. GlobalMultiuse classes are highly un-reccomended in MTS.
For ActiveX EXE only:
SingleUse: This has nothing to do with Object definition (that it, this setting does not map to any IDL setting). The effect of this setting is to run a different new process hosting the COM object for each requested instance of the object. In other words, as the MSDN states, for such classes once an application is connected to the class object with CoGetClassObject, the class object is removed from public view so that no other applications can connect to it.

5) A few words about COM threading models

 

In COM objects live in apartments.
There are at present 2 kinds of apartments available:
- Single Threaded Apartments (STA)
- Multi Threaded Apartments (MTA)

A third one will be in W2k: Neutral Threading Apartment. 
VB 7  (SPx) maybe will support it.

In an STA there is just 1 thread running, it loops in an hidden window message pump waiting for dispatching method calls to objects living in the STA.
There is no limit in the number of objects that can be in an STA.
There is no limit in the number of STA that can be in a process.
Since there is only one thread for all the objects that live in a specific STA, when an object is running into a method call all requests for other objects are blocked (there is support for re-entrancy (callbacks) anyway).
Thus in this case there are no syncronization issues at class level.
However if you are developing in C++,you still have to protect datas at module level.
If you are developing in VB there is no such problem since variables declared in .bas are not really at module level, they are thread (thus apartment) specific since they are stored in TLS (thread local storage).

In an MTA there is a pool of threads running, waiting to serve incoming calls. In this case Syncronization is an issue. You have to deal with OS syncronization primitives such Mutexes, Semaphore, Critical Sections, etc ..
There can be just one MTA per process.

A COM object declares to the COM runtime its apartment requirements via the ThreadingModel key under the object's CLSID in the registry. These are the possible entries for this key:

- no ThreadingModel entry : The object will be put in the Main STA (a "special" STA, the first one created in the process)
- Apartment : The object will be put in an STA. The same one of the caller if the caller is in an STA yet, if not (that is the caller is in an MTA) a new STA will be created (or it will be put in an existing one).
- Free : the object will be put in an MTA
- Both : the COM run-time will put the object in an MTA or STA, depending on what type of apartment the caller is.

6) How do I overcome VB's limits of the binary compatibility settings ?

Among the different COM details that VB hides to the programmers, one of the most problematic one is that it doesn't let you take control on the interface GUIDs. Via the binary compatibility settings VB tries to force you follow COM rules (as a fact breaking such rules with the interface foward mechanism it silently implements when you add a method to a class). In any case VB support in Interface definition and maintenance is definetely lacking when you have to design and implement large COM based systems.

Here i place to links where is illustrated (the same in both artcicles) a tecnique that lets overcome such limits. This approach may be scaring at first to VB programmers that are not that familiar with C language, cause you have to go a little deep into IDL, nevertheless, trust me, that, after the first little drop of you development speed, you will get much more robust and well designed applications. 

http://msdn.microsoft.com/library/techart/msdn_bldvbcom.htm 
http://www.microsoft.com/msj/0100/versioning/versioning.asp

One note: instead of using Oleviewer to reverse-engineer the Type library i suggest you to use the TypeLibrary Viewer from http://www.vivid-creations.com/ cause Oleviewer does not always generate IDL that can be immediately compiled by midl to generate the type library without a certain number of adjustments. I verified that this tool i suggest generates much more ready-to-compile IDL

 

7) A few words about Byref / ByVal - Standard marshaling / Custom Marshaling By Value
There is quite a lot of misunderstanding in the VB-COM "newbie" comunity, about what happens underneath when a COM interfaces is passed as method parameters. As in most of the cases VB programmers are hidden/protected about a lot of details on the COM run-time, but you do have to know such details if you want to assemble something that perform decently in a distributed environment.

1) Basic automation types (double,long,string) are passed  by value if the method signature is declared byval. They  are passed by reference  if the method signature is declared byref. The behviour you experience is the same if the method call takes place in-Apartment or cross-Apartment.

2) COM Interfaces: The first thing to stress here is that in VB (or in any COM aware language) you never get in touch with a Class (the client doesn't have to know the class binary layout ! ), you get in touch with interfaces. So when 'you' think to pass an "object" you are actually passing an interface. The 'state' of underlying COM object that is behind the interface is not copied (e.g. as a struct (type) is).  

Now, coming to the point: 
COM Interfaces are passed
a) by reference
if the method signature is declared byval 
b) by a reference to a reference if the method signature is declared byref.

to see this i show here the mapping between a VB method signature and the underlying IDL

public sub test(byval a1 as IMyInterface1,byref a2 as IMyinterface2) ->
HRESULT test([in] IMyInterface* a1,[in,out] IMyInterface2** a2);

This means that, even if you pass an interface byval, you don't have to be surprised that, on returning from the method call, one of its properties have been changed. 
Someone may ask where byref makes the difference: Passing an interface byref let the callee replace the object behind the interface with another object that implements the same interface. 

example:
----------------------------------------------------------

CFastAnimal and CSlowAnimal both implement the interface IRun 
CSomething implements IPrepareToRun. IprepareToRun has to methods : preparebyref and preparebyval, where, as you may guess, 
the former is 
public sub preparebyref(byref a1 as IRun) 
set a1 = Createobject("CSlowAnimal")
a1.name = "AAA"
end sub

the latter is 
public sub preparebyval(byval a1 as IRun) 
set a1 = Createobject("CSlowAnimal")
a1.name = "AAA"
end sub

-----------------
THE CLIENT

sub a()
dim x as IRun
set x = createobject("CFastAnimal") 
x.name ="BBB"
dim y as IPrepareToRun
set y =createobject("CSomething") 
y.preparebyref(x) 
x.run ''RUNS SLOW
debug.print x.name 'PRINT "AA"
end sub

sub b()
dim x as IRun
set x = createobject("CFastAnimal") 
x.name ="BBB"
dim y as IPrepareToRun
set y =createobject("CSomething") 
y.preparebyval(x)
x.run 'RUNS FAST
debug.print x.name 'PRINT "AA"
end sub 

----------------------------------------------------------

You experience the same behaviour described above if you pass Interfaces in-Apartment / or cross-Apartment (Cross Process / Cross Host) if such interface are standard marshaled (Type-Library marshaling).
I should stress however that passing interfaces across apartment boundaries is very bad design (expecially across hosts): reading or setting a property involves a network roundtrip. There are somecases where you may need to pass an interface for a callback, but NEVER use an interface as a method to pass/return data among two objects if you are not sure the 2 objects AND the interface in question will always be in the same apartment. 
OK, i said never, now i correct myself, there is an execption: you can do it if the interface implements MBV (Marshaling by value), and , yes, ADO disconnected Recordsets do implement marshaling by value: This means that when the _Recordset interface cross apartment boundaries the data behind the recordset are copied in the new apartment. If the parameter is declared byref, then the data are copied back. 
BUT rememeber this happens only if the recordset cross apartment boundaries, if it's not the case you get the standard COM behaviour. 
Conclusion: 
1) you have to know what standard marshaling is to develop distributed application  
2) you have to know when an interface MBV, and be aware that, in such case, your object behaves differently when is passed in-Apartment or inter-Apartment.


8) How do I implement proper Error Handling in VB ?
According to my personal experience, error handling code does not deserve sufficient attention and standardization within VB developers group. Most time each programmer is left free to implement his error handling style (and most times the style is "no error handling at all").
I've heard someone saying: "we will implement error handling later", I came to know that, in this case, "later" meant a week-end at work for this guy. This was something I liked :), too many times I had to stare at an "ActiveX can't create object" .. asking myself where the hell this message came from. 

Implement proper error handling, according to me, means:

  • provide sufficient information about where and why an error occurred
  • provide clean up code (to free resources) that is executed in " error" and " non error" conditions


I suppose you have already read through the MSDN help about 
On Error goto , Resume, Goto , etc ..
If it's the case, I think you have by now almost all the elements you need, still, you 
could have missed an important detail: 

setting up an Error Handler while another Error Handler is active has no effect.


example:

dim d as integer
on error goto errorhandler
d=1/0
exit sub
errorhandler:
on error goto errorhandler2 'ineffective
d= 1/0
exit sub
errorhandler2:
debug.print "You'll never get here"
end sub


You may have heard that VB doesn't provide you the best constructs to implement proper Error Handling "naturally". As a fact, the next version of VB (VB.NET or whatever) will expose try/catch/finally constructs, that provide more robust error handling code.
Let's see how you can get robust error handling with the ante VB.NET features available.

Errors info can be propagated in two ways:
- via the structure provided by COM (Com Exceptions, that is err.raise in VB)
- provide your app a custom channel to communicate errors: typically a return code and a string that
contains error info.

Which of the two options is best ? I think it's a question of personal taste. 
The second approach generates a code pretty full of nested if, that you may consider nice or ugly. 
I personally prefer the 2nd one; still most samples you find use the 1st approach that is "officially" considered a better option. 
By the way, as far as I remember, there has always been at least one bug hanging around the COM Exception mechanism that you have to work around, e.g.: 

  • Ado cleaning up the Err object 
  • The snake (~) error that pre- SP1 COM+ put in the error description when you called SetAbort.

So .. stay tuned with the MS KB site if you go for the official way :)  

A basic Function implementation skeleton can be defined in the following way:

Propagate via Com Exception

Public Sub MyFunc() 
On Error GoTo ErrorHandler
Dim l_ErrNum as Long
Dim l_ErrDescr as String
' Your code goes here
CleanUp:
On Error Resume Next
'Release resources, Close ADO objects, etc ..
if Not l_ErrNum = 0 then
    'You can do a Select Case on the Error Code 
    'to provide specific behaviour according to the Error code
    GetObjectContext.SetAbort
    On Error goto 0 'Disable "Resume Next" Error Handler
    'Propagate Error.
    'You can Add extra info to the error Description, like the Call Stack Chain
    Err.Raise l_ErrNum,,l_ErrDescr + " -- " & " MyClass :: MyFunc"
Else
    GetObjectContext.SetComplete
End If
Exit Function
' Error handling.
ErrorHandler:
'Pick up the Error 
'Store Error info in local vars to avoid COM/ADO bugs
l_ErrNum = Err.Number
l_ErrDescr = Err.Description
Resume CleanUp
End Function


Propagate via Return code

Public Sub MyFunc(byref p_ErrorDescription) as Long 
On Error GoTo ErrorHandler
Dim l_ErrNum as Long
Dim l_ErrDescr as String
' Your code goes here
CleanUp:
On Error Resume Next
'Release resources, Close ADO objects, etc ..
if Not MyFunc = 0 then
    'You can do a Select Case on the Error Code 
    'to provide specific behaviour according to the Error code
    'You can Add extra info to the error Description, like the Call Stack Chain
    p_ErrorDescription= p_ErrorDescription + " -- " & " MyClass :: MyFunc"
    GetObjectContext.SetAbort
Else
    GetObjectContext.SetComplete
End If
Exit Function
' Error handling.
ErrorHandler:
'Pick up the Error and eventually do some processing
MyFunc= Err.Number
p_ErrorDescription= Err.Description
Resume CleanUp
End Function

It's better to replace the return type from Long to an Enum of application defined error codes. They have to be published in the component Type-Library so that they are accessible by the client as well.  This approach gives the following advantages:

  1. On the client side it's easier to implement logic depending on the return codes thank to the Intellisense technology,  
  2. Error codes definition are not duplicated in the client program, so there is no danger that they get out of sync.

The same approach is good when you use Com Exceptions as well. You don't get 1, but you still get 2

I've developed a little VB-Addin that generates automatically the skeleton of "Return code" type Error handling.
Still it's really easy to customize it to produce Error Handling code of Type "Com Exception"
If you are interested, mail me and I'll post it to you.

This is just a super-fast introduction to the subject. You may want to read:

Example of code structure for COM+ component

Building a Better Mousetrap for COM+ (MTS)

published at the VB2TheMax site to get deeper details. 


9) Can I store Interface pointers in the SPM ?
No, you can't !! the SPM is unaware of Apartment marshaling issues.If you ask the SPM for an interface pointer while running in an aprtment that is different from the one where the interface pointer resides the SPM will blindly pass you the rough interface pointer without marshaling it, thus violating COM rules (and your app will randomly fail). Park the interface pointer in the GIT and park the GIT cookie in the SPM. 


10) Binary Compatibility of components
When you decide to issue a new version of an MTS component you should take care of its compatibility with the previous version.  There are three types of changes you could make:
1) You change only internal code of a component, keeping the interface that the component provides untouched.
2) You only add a member(s) (method, property, enum etc.) to the interface, but leave declaration of previously declared members quite the same.
3) You decided to revise the interface members and some of them are changed or even removed.

Thus, if you're building a component in VB it's strongly recommended to set Version Compatibility of the project to Binary Compatibility mode. In this case VB will track the changes of the interface(s) and will notify you if you make changes of type (3), but not 2 !
To install a new version into MTS you should do the following: For changes of type (1) you can simply overwrite the older DLL with the newer one.  The remote clients that had the client export registered need no intervention, but with both (2) and (3) types of changes you should completely reinstall the component (i.e. remove it from MTS and then install it back), you then need to re-export the package and have the remote clients run the client installation again (remove the old one before from Control Panel/Install remove applications).
Don't forget to shut the component's package down to unlock components' DLLs. It's possible to shut a package down remotely and perform other administration operations "programmatically" (see MTS2.0 Admin Type Library)

11) Object Pooling
An excerpt from MTS documentation http://msdn.microsoft.com/library/psdk/mts20sp1/mtxpg04_8v6v.htm :
"Object pooling and recycling is not available in this release. MTS calls CanBePooled as described here, but no pooling takes place. This forward-compatibility encourages developers to use CanBePooled in their applications now in order to benefit from a future release without having to modify their applications later."


This article refers to version 2.0 Service Pack1 of MTS and applies to Windows NT 4.0 system. Object pooling is a COM+ feature and is available under Windows 2000 platforms.

It will be available for objects that declare a new threading model : the neutral threading apartment (nta) . Such objects don't have to have any thread affinity, and will have to enlist manually into transactions

 

Comments, suggestions .. whatever .. drop me a line if you wish 

 

You are visitor number Since 2001/01/01