There is quite a lot of misunderstanding among COM newbies about what really happens when a COM interface is passed as a method parameter. As in most of the cases, VB programmers are protected from 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 behavior you experience is the same if the method call takes place in-Apartment or cross-Apartment.
2) COM Interfaces: The first thing to emphasize 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!); instead, you get in touch with interfaces. So when you think you’re passing 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 (a VB 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.
For example, the following VB procedure signature:
Public Sub test(ByVal a1 As IMyInterface1, ByRef a2 As IMyinterface2)
maps to the following IDL code:
HRESULT test([in] IMyInterface* a1,[in,out] IMyInterface2** a2);
This means that even if you pass an interface ByVal, you shouldn’t to be surprised that, on returning from the method call, one of its properties have been changed.
You might wonder when ByRef makes the difference: Passing an interface ByRef lets the callee replace the object behind the interface with another object that implements the same interface. Here an example: Say that CFastAnimal and CSlowAnimal both implement the interface IRun. CSomething implements IPrepareToRun, which has two methods : PrepareByRef and PrepareByVal:
Public Sub PrepareByRef(ByRef a1 As IRun) Set a1 = CreateObject("CSlowAnimal") a1.name = "AAA"End SubPublic Sub PrepareByVal(ByVal a1 As IRun) Set a1 = Createobject("CSlowAnimal") a1.name = "AAA"End Sub
Here is the client code:
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 'prints "AA"End SubSub 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 'prints "AA"End Sub
You see the same behavior 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 considered bad design (especially across hosts): reading or setting a property involves a roundtrip. There are some cases 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 two objects and the interface in question will always be in the same apartment.
There is an exception, though: 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 crosses 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 remember: this happens only if the recordset cross apartment boundaries, if it’s not the case you get the standard COM behavior.
The conclusions are:
1) you have to know what standard marshaling is to develop distributed application;
2) you have to know when an interface is MBV and be aware that, in such case, your object behaves differently when is passed in-Apartment or inter-Apartment.