Designing Remote Interfaces

One of the best things about COM is that it hides the details of interprocess communication. The code you write for an in-process object will automatically work for an out-of-process object. You don't have to change anything in your class module. The client-side code that you write doesn't care whether the object is close by or far away.

The hiding of interprocess communication details from both objects and clients is known as location transparency. As you saw in earlier chapters, this powerful feature is made possible by the universal marshaler and its ability to read interface definitions from type libraries at run time. Figure 6-1 shows how the proxy and the stub are built at run time. When an interface reference is marshaled from one process to another, the universal marshaler builds these helper objects by inspecting the interface definition in a type library.

Code written for an in-process object automatically works in an out-of-process object, but it won't necessarily be efficient. Many techniques that work efficiently for in-process objects yield unacceptable results when taken out of process. The overhead of the proxy/stub layer becomes a significant consideration during the design phase of an application. When you begin to draft a set of remote interfaces, you must consider several factors that aren't part of a classic OOP design methodology.

When a client invokes a method, it passes control to the object for the duration of the call. Every call in which control is passed to the object and then back to the client is called a round-trip. When a method is invoked on an object in another process, the round-trip must go across a proxy/stub layer. This means that every out-of-process method call requires a thread switch and the marshaling of data between the client's process and the object's process.

click to view at full size.

Figure 6-1. The universal marshaler creates a proxy and a stub when an interface reference is marshaled across processes. If the client and the object are running on different machines, each computer requires a local copy of the type library.

Figure 6-2 shows a table with benchmarking statistics for method calls in three different scenarios: in-process, out-of-process, and remote. These benchmarks were measured using empty methods to demonstrate differences in the overhead of method invocation. You can see that the round-trip time increases by a factor of about 1000 as you move the object out of process. When you take an out-of-process object and move it onto another machine, you take another significant performance hit.

Figure 6-2. Benchmarks for calling empty methods in Visual Basic servers on a 166-MHz Pentium machine.

Benchmark Test In-Process Calls per Second Out-of-Process Calls per Second Remote Calls per Second
Method with a 4-byte ByVal parameter 129,041 1765 495
Method with a 100-byte-array ByRef parameter 119,240 968 342

If the client machine and the server machine are physically distant, you encounter yet another frustrating obstacle: the speed of light! For example, let's say that a round-trip between a computer in New York and one in Los Angeles is 5000 miles (2500 miles in each direction). If a method call were propagated across the wire at the speed of light, you would experience about 37 calls per second (186,000 miles per second divided by 5000 miles per call). The actual transmission speed of a remote COM method is, of course, less than the speed of light. In reality, you would experience fewer than 37 calls per second between these cities.

The lesson from these benchmarks is that round-trips are inherently evil. Your mission as a COM programmer is to eliminate the need for excessive round-trips. When you design a class or a user-defined interface, you should never expose three public methods or properties when a single method can accomplish the same amount of work. If your class exposes three public properties, a remote client will require three round-trips to access all the data. When a remote interface design isn't very efficient, the client code begins to looks like this:

 Dim Rectangle As CRectangle Set Rectangle = New CRectangle ' initialize object Rectangle.X = 100 Rectangle.Y = 100 Rectangle.Width = 300 Rectangle.Height = 200 

You can see from Figure 6-2 that marshaling a complete set of data in a single round-trip is much better than marshaling smaller amounts of data in multiple round-trips. If you need to move many bytes across the network to complete an operation, you should try to create a single signature that lets you do it in one shot. How can you improve the interface for the CRectangle class from the previous example? If you create a SetRectangleData method that takes four input parameters, the client can initialize the object in a single round-trip, like this:

 Rectangle.SetRectangleData 100, _                            100, _                            300, _                            200 

Choosing Between ByRef and ByVal

When you create an interface, you should first think about optimizing round-trips. Then you should tune the parameter declarations in your methods to optimize the packets of data that move across the network. Some parameters must be pushed from the client to the object, and others must be pulled from the object back to the client. Some parameters need to move in both directions. Look at the following three method signatures, and think about how the universal marshaler interprets each one:

 Sub Sammy(ByVal X As Double) Sub Frank(ByRef Y As Double) Sub Dean(Z As Double) 

The method Sammy pushes the X parameter from the client's process to the object's process. If you want to marshal your parameters in one direction only, you should declare them ByVal. The method Frank defines a ByRef parameter that pushes the data from the client to the object and then pulls the data from the object back to the client. A ByRef parameter always results in the data being marshaled both to and from the object. The method Dean does the same thing as Frank because Visual Basic's default passing convention is ByRef.

Many Visual Basic programmers incorrectly assume that ByRef is always more efficient than ByVal. In an in-process call, a ByRef parameter is simply passed as an address to the data that's being referenced. In an out-of-process call, ByRef is always more expensive because the universal marshaler must also transmit the data that is being referenced. This means that ByVal is more efficient for parameters in remote interfaces. Use ByRef parameters only when you need to, such as when you need to move data from the object's process back to the client's process.

Now you might wonder, "How can I simply pull data from the object back to the client?" Unfortunately, Visual Basic doesn't make it straightforward. COM and IDL define parameters as [in], [out], or [in, out]. Visual Basic doesn't know how to deal with the COM [out] parameters. The only way to explicitly pull data back from the object is to use a method's return value. The problem with return values is that you can define only one for each round-trip. You're usually better off defining several ByRef arguments in a single method. You simply have to tolerate the fact that you're pushing unnecessary data from the client to the object. The following method signature shows the most efficient way to pull several pieces of data back from the object in a single round-trip:

 ' Defined in class CRectangle Sub GetRectangleData( _      ByRef X As Long, _     ByRef Y As Long, _     ByRef Width As Long, _     ByRef Height As Long) 

To call this method from a client, you must create local variables of the correct type and pass them in the call to GetRectangleData:

 Dim X As Long, Y As Long Dim Width As Long, Height As Long Rectangle.GetRectangleData X, _                            Y, _                            Width, _                            Height 

After you call GetRectangleData, the local variable is populated with the data sent from the object. If you understand Visual Basic's inability to handle [out] parameters, it's simple to choose between ByVal and ByRef. You simply decide which way you want to move the data. The same rules apply for every primitive data type.

Special Primitive Data Types

With strings, arrays, variants, and UDTs, Visual Basic and the universal marshaler treat complex data types as simple primitive data types. When you declare a method parameter as one of these types, the universal marshaler can figure out how to marshal the required data across the proxy/stub layer. However, this works correctly only if these special data types carry additional metadata that describes their payloads. The universal marshaler reads this metadata at run time to determine how to create the required marshaling packets to send across the network.

The first of the four special primitive types is the Visual Basic for Applications (VBA) string. A VBA string is defined in IDL as a basic string (BSTR). A BSTR is a complex data type that is represented in memory by a cached string length value and a zero-terminated character array. (Characters are stored in Unicode, not ANSI.) The universal marshaler reads the length value of the BSTR to determine how many characters to send across the wire. Whether you declare a string parameter as ByRef or ByVal, all the data is automatically marshaled across the proxy/stub layer.

The second special primitive type is the VBA array, which is defined in IDL as a SAFEARRAY. A SAFEARRAY keeps track of not only the data for the actual array but also the upper and lower bounds of the array and other metadata that describes the data in the array. The universal marshaler can marshal a complete array in either direction in a single round-trip. You can declare a method signature with a ByRef array parameter, like this:

 Sub SendValues(ByRef Value() As Integer) 

Using Visual Basic 5, you can pass dynamic arrays if you declare them ByRef. This means that you can move an array in either direction. Using Visual Basic 6, you can also use an array type as the return value for a function. When you marshal arrays, you should be careful to pick the most efficient data type. An array of Doubles (8 bytes times n elements) generates four times the amount of network traffic as an array of Integers (2 bytes times n elements). Using an Integer data type can result in significant savings if you move arrays that have hundreds or thousands of elements.

You must be careful when you declare method parameters based on arrays. All parameters that pass arrays must be declared as ByRef. This means that the contents of the array will be marshaled across the network in both directions on every call. The problem is that generally you want to move the array only in a single direction. You want to move the array either from the client to the object or from the object to the client. You should always use dynamic arrays instead of static arrays because then you can pass an empty array in the direction you don't care about. Let's look at the two cases that are possible when you want to move the array in a single direction.

In one case, you want to pass an array from the object back to the client. As long as you use a dynamic array and don't use the ReDim statement in the client, you can send an empty array to the object. The object can use the ReDim statement to size the array. The object can then populate the array and return it to the client. Remember, with Visual Basic 6 you can use a method's return value to send an array to the client.

In the other case, you want to pass an array from the client to the object. This is where it's easy to make mistakes. You must be careful not to pass the array from the object back to the client when the call returns. Once the array has been marshaled to the object and the object has finished using it, you should use either the Erase statement or the ReDim statement to remove all the elements from the array before it is returned to the client.

The third special primitive type is the variant, a self-describing data type that is recognized by both VBA and IDL. As you will recall, this data type is the key to making automation and IDispatch work. A variant can hold anything—an integer, a double, a string, or even an array. When you marshal a variant, the universal marshaler transmits all the data associated with it, even if the variant references something complex, such as a string or a dynamic array. You can get pretty tricky when you marshal variants. Examine the following method implementation:

 Sub GetData(ByRef Data As Variant)     ' (1) Name (2) Age (3) Children     Dim Temp() As Variant     ReDim Temp(1 To 3)     Temp(1) = "John"     Temp(2) = CLng(34)     Dim Children() As String     ReDim Children(1 To 3)     Children(1) = "Bob"     Children(2) = "Martha"     Children(3) = "Wally"     Temp(3) = Children     Data = Temp End Sub 

This method packs up a fairly complex data structure, which is depicted in Figure 6-3. A top-level variant array holds three elements: a string, a long, and a string array. When you assign the top-level variant array to a ByRef parameter, the universal marshaler sends the entire structure across the network and re-creates it on the other side. The client that calls this method must be able to deal with the complexities of the data structure, but you can see that it's still a powerful way to get lots of data across the network in a single round-trip.

click to view at full size.

Figure 6-3. You can marshal this complex data structure in either direction using a single variant parameter or a variant return value. If you use techniques such as this, your code must be able to deal with the complexity on the client side as well as the object side.

The last special primitive type is the VBA user-defined type (UDT). The ability to marshal these custom data types first became available with Visual Basic 6. Not only do you need Visual Basic 6 to marshal these UDTs, you also need a version of the universal marshaler that is available only in later versions of COM, such as versions included with Microsoft Windows 98, Windows NT 5, and Windows NT 4 with Service Pack 4. Earlier versions of the universal marshaler don't recognize a UDT as variant compliant, so you can't use UDTs in COM method calls in an IDispatch interface or a dual interface. You can use UDTs in your methods only if your code will run on computers that have the newer version of the universal marshaler. (The universal marshaler is in the system file OLEAUT32.DLL.)

A UDT is the VBA equivalent of a structure in C. If you define a UDT in a public class module, Visual Basic automatically publishes it in your type library. You can create a UDT in a public class module like this:

 Type CustomerDataType     FirstName As String     LastName As String     Phone As String     MailingList As Boolean     MailingAddress As String End Type 

In the equivalent IDL, shown below, you can see that each typedef has an associated GUID that identifies its data layout scheme. A UDT can hold any collection of variant-compliant data types.

 typedef     [uuid(A2B7676A-EAD1-11D1-98F9-00600807E871)]     struct tagCustomerDataType {         BSTR FirstName;         BSTR LastName;         BSTR Phone;         VARIANT_BOOL MailingList;         BSTR MailingAddress;     }CustomerDataType; }; 

After you define the UDT in a public class module, you can use it to define ByRef parameters of that type. For instance, you can create methods such as InitCustomer and GetCustomer that use the UDT to marshal data across the proxy/stub layer. Take a look at the following class definition for the CCustomer class:

 Private FirstName As String Private LastName As String Private Phone As String Private MailingList As Boolean Private MailingAddress As String Sub InitCustomer(ByRef Customer As CustomerDataType)     FirstName = Customer.FirstName     LastName = Customer.LastName     Phone = Customer.Phone     MailingList = Customer.MailingList     MailingAddress = Customer.MailingAddress End Sub Function GetCustomer() As CustomerDataType     GetCustomer.FirstName = FirstName     GetCustomer.LastName = LastName     GetCustomer.Phone = Phone     GetCustomer.MailingList = MailingList     GetCustomer.MailingAddress = MailingAddress End Function 

As you can see, a UDT lets you move several pieces of related data across the wire using a single parameter. A client application can use the InitCustomer method like this:

 Private Customer As New CCustomer Private Sub cmdSend_Click()     Dim Temp As CustomerDataType     Temp.FirstName = txtFirstName     Temp.LastName = txtLastName     Temp.Phone = txtPhone     Temp.MailingList = IIf(chkMailingList.Value = vbChecked, _                            True, False)     Temp.MailingAddress = txtMailingAddress     Customer.InitCustomer Temp End Sub 

A client can call the GetCustomer method like this:

 Private Sub cmdReceive_Click()     Dim Temp As CustomerDataType     Temp = Customer.GetCustomer()     txtFirstName = Temp.FirstName     txtLastName = Temp.LastName     txtPhone = Temp.Phone     chkMailingList.Value = IIf(Temp.MailingList, _                                vbChecked, vbUnchecked)     txtMailingAddress = Temp.MailingAddress End Sub 

As you can see, Visual Basic and the universal marshaler do an amazing amount of work for you behind the scenes. The benefit to you as a Visual Basic programmer is that you can use these special data types as if they were simple primitives. You can declare parameters of any of these data types and know that your data will be sent across the wire when the time is right.

C++ programmers, on the other hand, have it much harder. A C++ developer who wants to create components that will be used by Visual Basic programmers must also use BSTRs, SAFEARRAYs, and VARIANT types. These types aren't natural to a C or a C++ programmer. Many macros and convenience functions are available for dealing with these special types, but it takes most C++ programmers some time to get up to speed.

Programming Distributed Applications With Com & Microsoft Visual Basic 6.0
Programming Distributed Applications with Com and Microsoft Visual Basic 6.0 (Programming/Visual Basic)
ISBN: 1572319615
EAN: 2147483647
Year: 1998
Pages: 72
Authors: Ted Pattison © 2008-2017.
If you may any questions please contact us: