Microsoft KB Archive/321695

= HOW TO: Wrap a UCOMIStream in a Stream Class in Visual Basic .NET =

Article ID: 321695

Article Last Modified on 5/16/2007

-

APPLIES TO


 * Microsoft .NET Framework 1.0
 * Microsoft .NET Framework 1.1

-



This article was previously published under Q321695



IN THIS TASK
 SUMMARY

Create an Unmanaged COM DLL

Create a Managed Application

Implement the New Class

Use the Class to Work with the Stream

 REFERENCES



SUMMARY
This step by step article describes how to wrap a UCOMIStream interface in a class that derives from the abstract Stream class. By using this technique, a single class can be used to create instances of objects that function in the context of either a UCOMIStream interface or as Stream objects. Without such a class, data must be copied to and from these different sources and then maintained between them. This article describes how to use objects of this class in a variety of contexts.

For demonstration purposes, this article describes how to consume and how to modify an IStream pointer (from the unmanaged COM world) in .NET by using a new class that you develop. First, you must generate a simple unmanaged DLL that returns an IStream pointer to the caller. Because Microsoft Visual Basic .NET cannot generate unmanaged DLLs, Microsoft Visual C++ .NET is used to create the DLL. Then, you must create a managed application that references this COM DLL. In the managed application, you can develop a new class that derives from the Stream class and then wraps a UCOMIStream interface. Through COM Interop, create a reference to the unmanaged COM DLL and then work with the stream by using the new class. This article also demonstrates how to pass objects of the class to other stream functions such as the StreamReader class and the StreamWriter class.

back to the top

Create an Unmanaged COM DLL
Generally, you already have a COM server that you want to interoperate with. However, for the sake of completeness, this article includes steps to create a COM server.  Open Visual Studio .NET. On the File menu, click New and then click Project to display the New Project dialog box. In the Project Type window, click Visual C++ Projects. In the Templates window, click ATL Project. For the name of the project, type UnmanagedFuncs and then click OK to create the project.</li> When the ATL Wizard dialog box appears, click Finish to generate the files for the project.</li> On the View menu, click Class View. In the Class View window, right-click the UnmanagedFuncs project, click Add, and then click Add Class to display the Add Class dialog box.</li> In the Categories window, click Visual C++. In the Templates window, click ATL Simple Object. Click Open to open the ATL Simple Object Wizard dialog box.</li> Type SimpleObj in the Short Name field and then click Finish to add the object to the project.</li> In the Class View window, expand the UnmanagedFuncs project. Right-click the ISimpleObj interface, click Add, and then click Add Method to open the Add Method Wizard.</li> In the Add Method Wizard, type GetUnmanagedData in the Method Name field.</li> In the Parameter Type combo box, type IStream**. In the Parameter Name field, type ppData .</li> Click out in the &quot;Parameter Attributes&quot; section. Make sure that this is the only check box that is selected.</li> Click Add to add this new parameter and then click Finish.</li> In the Class View window, expand the UnmanagedFuncs project and then expand the CSimpleObj class. Double-click the GetUnmanagedData method that was just created.

This opens the source code for this method.</li>  Replace the implementation of this method with the following code: STDMETHODIMP CSimpleObj::GetUnmanagedData(IStream** ppData) {   HRESULT hr; hr = CreateStreamOnHGlobal(0, TRUE, ppData); if (FAILED(hr)) return hr;

ULONG lBytesWritten; hr = (*ppData)->Write(&quot;This is unmanaged data.&quot;, 24, &lBytesWritten); if (FAILED(hr)) return hr;

return S_OK;

} </li> On the Build menu, click Build UnmanagedFuncs.</li></ol>

back to the top

Create a Managed Application
In this section, you create a new managed application. As part of this process, a new class is developed that descends from the Stream class and then wraps a UCOMIStream interface. This class works with streams such as the one that is returned from the unmanaged COM DLL that was just created.
 * 1) Open Visual Studio .NET.
 * 2) On the File menu, click New and then click Project to display the New Project dialog box.
 * 3) Under Project Type, click to select Visual Basic Projects. Under Templates, click Console Application. In the Name field, type ManagedTestApp and then click OK.
 * 4) In Solution Explorer, right-click the ManagedTestApp project and then click Set as StartUp Project.
 * 5) In Solution Explorer, right-click the ManagedTestApp project and then click Add Reference.
 * 6) In the Add Reference dialog box, click to select the COM tab.
 * 7) Click Browse and then move to the UnmanagedFuncs.dll that you created in the previous step. Double-click the UnmanagedFuncs.DLL file to add the file to the &quot;Selected Components&quot; section of the Add Reference dialog box. Click OK to add the reference to your application.
 * 8) In Solution Explorer, expand the References node and then verify that UnmanagedFuncs has been added.
 * 9) In Solution Explorer, right-click ManagedTestApp, click Add, and then click Add Class.
 * 10) Under Categories, click Local Project Items. Under Templates, click Class. In the Name field, type ComStream.vb and then click Open.
 * 11) On the Build menu, click Build ManagedTestApp.

back to the top

Implement the New Class
Because the new class is derived from the Stream class, which is a MustInherit class, several methods must be implemented. A private member that is a UCOMIStream interface must also be added. This stream interface enables the creation of instances of this class from existing IStream interfaces, which is demonstrated later in this article. <ol> In Solution Explorer, expand the ManagedTestApp project.</li> Double-click the ComStream.vb file to open the file in the editor.</li>  Replace the contents of ComStream.vb with the following code: Imports System.IO Imports System.Runtime.InteropServices

Public Class ComStream Inherits Stream

'This is the reference to the stream used by this class. Private theOrigStream As UCOMIStream

Public Sub New(ByRef theStream As UCOMIStream) If (theStream Is Nothing) Then Throw New ArgumentNullException(&quot;theStream&quot;) Else theOrigStream = theStream End If  End Sub

Public Overrides Function Read(ByVal buffer As Byte, ByVal offset As Integer, ByVal count As Integer) As Integer If (theOrigStream Is Nothing) Then Throw New ObjectDisposedException(&quot;theStream&quot;) End If

Dim iBytesRead As Integer = 0 Dim boxBytesRead As Object = iBytesRead Dim hObject As GCHandle

Try hObject = GCHandle.Alloc(boxBytesRead, GCHandleType.Pinned) Dim pBytesRead As IntPtr = hObject.AddrOfPinnedObject

If offset <> 0 Then Dim tmpBuffer(count - 1) As Byte theOrigStream.Read(tmpBuffer, count, pBytesRead) iBytesRead = CInt(boxBytesRead) System.Array.Copy(tmpBuffer, 0, buffer, offset, iBytesRead) Else theOrigStream.Read(buffer, count, pBytesRead) iBytesRead = CInt(boxBytesRead) End If

Finally If (hObject.IsAllocated) Then hObject.Free End Try

Return iBytesRead

End Function

Public Overrides Sub Write(ByVal buffer As Byte, ByVal offset As Integer, ByVal count As Integer) If (theOrigStream Is Nothing) Then Throw New ObjectDisposedException(&quot;theStream&quot;) End If

If offset <> 0 Then Dim bufferSize As Integer bufferSize = buffer.Length - offset Dim tmpBuffer(bufferSize) As Byte System.Array.Copy(buffer, offset, tmpBuffer, 0, bufferSize) theOrigStream.Write(tmpBuffer, bufferSize, Nothing) Else theOrigStream.Write(buffer, count, Nothing) End If

End Sub

Public Overrides Function Seek(ByVal offset As Long, ByVal origin As SeekOrigin) As Long If (theOrigStream Is Nothing) Then Throw New ObjectDisposedException(&quot;theStream&quot;) End If

Dim lCurPosition As Long = 0 Dim boxCurPosition As Object = lCurPosition Dim hObject As GCHandle

Try hObject = GCHandle.Alloc(boxCurPosition, GCHandleType.Pinned) Dim pCurPosition As IntPtr = hObject.AddrOfPinnedObject

theOrigStream.Seek(offset, origin, pCurPosition) lCurPosition = CLng(boxCurPosition) Finally If (hObject.IsAllocated) Then hObject.Free End Try

Return lCurPosition End Function

Public Overrides ReadOnly Property Length As Long Get If (theOrigStream Is Nothing) Then Throw New ObjectDisposedException(&quot;theStream&quot;) End If

Dim theStats As STATSTG theOrigStream.Stat(theStats, 1)

Return theStats.cbSize End Get End Property

Public Overrides Sub Flush If (theOrigStream Is Nothing) Then Throw New ObjectDisposedException(&quot;theStream&quot;) End If

theOrigStream.Commit(0) End Sub

Public Overrides Sub Close If (Not (theOrigStream Is Nothing)) Then theOrigStream.Commit(0) Marshal.ReleaseComObject(theOrigStream) theOrigStream = Nothing GC.SuppressFinalize(Me) End If  End Sub

Public Overrides ReadOnly Property CanRead As Boolean Get Return True End Get End Property

Public Overrides ReadOnly Property CanWrite As Boolean Get Return True End Get End Property

Public Overrides ReadOnly Property CanSeek As Boolean Get Return True End Get End Property

Public Overrides Property Position As Long Get Return Seek(0, SeekOrigin.Current) End Get Set(ByVal Value As Long) Seek(Value, SeekOrigin.Begin) End Set End Property

Public Overrides Sub SetLength(ByVal Value As Long) If (theOrigStream Is Nothing) Then Throw New ObjectDisposedException(&quot;theStream&quot;) End If

theOrigStream.SetSize(Value) End Sub End Class To examine this code, look first at the constructor of the class. To create an instance of an object from this class, the constructor must be passed as a UCOMIStream interface. If Nothing is passed, an exception is thrown. Without this interface, the class does not function properly. How you can pass an unmanaged IStream interface to create an instance of an object of this class is discussed later.

Before getting into the details of the Read method, you must understand that the UCOMIStream.Read method and the Stream.Read method are not completely compatible. The Stream.Read method supports the concept of reading data from a stream to a buffer at an offset other than zero in the buffer. The UCOMIStream.Read method does not support this concept because this method does not expect an offset parameter. This method assumes that it will read data from the stream and then put the data in the buffer at offset zero. If you understand this difference, the implementation of the Read method is simpler.

The first step that occurs is to verify that the UCOMIStream interface pointer is valid and then to generate an exception if the interface pointer is not valid. This same test is made in any method that uses the UCOMIStream interface. Next, the address of the variable must be pinned to successfully call the Read method of the private UCOMIStream interface. When this is complete, two conditions must be addressed. First, in the event where the offset is not zero, a temporary buffer of the size indicated by count (that is, the number of bytes to read) is created. Then, UCOMIStream.Read is called to read count bytes to the temporary buffer. Finally, the number of bytes that is read from the temporary buffer is copied to the offset in the buffer that is returned to the caller. Where the offset that is specified by the caller is zero, UCOMIStream.Read is called directly to perform the operation. In either case, you must use the CInt function to get the bytes-read value that is returned to the caller. Finally, because a handle is allocated for the object, the handle must be freed, as shown in the last section of this function.

The details of the Write method are similar to those of the Read method. Again, if there is a scenario where the offset that is specified is not zero, this must be handled. However, this time none of the variables has to be pinned because in the call to Write, Nothing can be passed to indicate that the number of bytes written is not required. If the number of bytes is required, then the variable must be pinned as you do in the Read method.

In the implementation of the Seek method, call the UCOMIStream.Seek method and then pass UCOMIStream.Seek the offset and the origin that was passed to you. The third parameter contains the position in the stream after the seek operation is performed. This is ultimately the value that is returned to the caller. Again, the variable is pinned so that the correct value is returned to the caller.

The implementation of the SetLength method translates to a call to the UCOMIStream.SetSize method.

The Flush method translates to a call to the UCOMIStream.Commit method. The zero that is passed in comes from the COM enumeration &quot;tagSTGC&quot;, which defines STGC_DEFAULT with a value of zero.

Now consider the Close method. Although this is not a pure virtual method, Close is required for the implementation to be fully functional. The first step is to commit the stream to storage. Next, use the static method ReleaseComObject in the Marshal class to release the reference to the interface that is provided in the constructor. Without this, the Runtime Callable Wrapper (RCW) maintains a positive reference count to the COM object and does not release. This results in a memory leak. Last, call the static method SuppressFinalize in the GC class. This prevents the garbage collector from calling the destructor again when it claims the memory for objects that are instantiated from the class. For more information about the GC.SuppressFinalize method and the Marshal.ReleaseComObject method, see the &quot;References&quot; section.

The properties CanRead, CanWrite, and CanSeek all return True because the stream class supports these operations.

The Length property, which is a read-only property, must do additional work to get the appropriate result. The UCOMIStream interface does not provide a Length method. Instead, UCOMIStream provides a Stat method that can be used to provide the equivalent value, which is in STATSTG.cbSize.

Finally, the Position property uses the Seek method to provide the functionality for this read/write property.

</li></ol>

back to the top

Use the Class to Work with the Stream
This section demonstrates how to use objects of the class in a variety of contexts. You can reference the COM DLL by using the managed assembly that was created previously, and you can use the IStream interface from the GetUnmanagedData function to create an instance of an object from the class. Then, you can work with this stream by using various managed functions. <ol> <li>In Solution Explorer, expand the ManagedTestApp project.</li> <li>Double-click the Module1.vb file to open the file in the editor.</li> <li> Replace the code in this file with the following code: Imports System Imports System.IO Imports System.Text Imports System.Runtime.InteropServices Imports UnmanagedFuncs

Module Module1

Sub Main Dim Strm As UnmanagedFuncs.IStream Dim myComObject As CSimpleObjClass Dim encoding As System.Text.Encoding = System.Text.Encoding.ASCII

'Get an instance of your unmanaged COM object and get some stream data from it. myComObject = New CSimpleObjClass myComObject.GetUnmanagedData(Strm)

'Create an instance of an object of your ComStream class. Dim StreamObj As ComStream StreamObj = New ComStream(Strm)

'Display the contents of the stream using the StreamReader class. Dim sr As StreamReader sr = New StreamReader(StreamObj) StreamObj.Seek(0, SeekOrigin.Begin) Console.WriteLine(&quot;---STREAM CONTENTS---&quot;) Console.WriteLine(sr.ReadToEnd)

'Add some data to the stream using the StreamWriter class. Dim sw As StreamWriter sw = New StreamWriter(StreamObj) sw.WriteLine sw.WriteLine(&quot;This is data added by using the StreamWriter class.&quot;) sw.Flush

'Display the contents of the stream using the StreamReader class. StreamObj.Seek(0, SeekOrigin.Begin) Console.WriteLine Console.WriteLine(&quot;---STREAM CONTENTS---&quot;) Console.WriteLine(sr.ReadToEnd)

'Try reading from the stream to a buffer with an offset other than 0. Dim myBuffer(100) As Byte StreamObj.Seek(0, SeekOrigin.Begin) StreamObj.Read(myBuffer, 10, myBuffer.Length) Console.WriteLine(&quot;---MYBUFFER CONTENTS---&quot;) Console.WriteLine(encoding.GetString(myBuffer))

'Try writing to the stream using a buffer offset that is not 0. Dim myNewDataBuffer As Char = {&quot;S&quot;, &quot;k&quot;, &quot;i&quot;, &quot;p&quot;, &quot; &quot;, &quot;A&quot;, &quot;d&quot;, &quot;d&quot;} '                                                 ^ This is offset 5 in the buffer. StreamObj.Write(encoding.GetBytes(myNewDataBuffer), 5, myNewDataBuffer.Length)

'Display the contents of the stream using the StreamReader class. StreamObj.Seek(0, SeekOrigin.Begin) Console.WriteLine Console.WriteLine(&quot;---STREAM CONTENTS---&quot;) Console.WriteLine(sr.ReadToEnd) Console.Write(&quot;Press Enter to continue&quot;) Console.Read End Sub

End Module Start with a COM Interop call to an unmanaged COM object that returns an IStream interface. Use this interface to create an instance of an object from the class. Display the contents of the stream as it is returned from the call to GetStreamData. The StreamReader class is used to read the contents of the stream and write the contents out to the console. Use the StreamWriter class to add data to the stream. Finally, use the stream reader to again display the full contents of the stream.

Next, demonstrate reading from the stream to a buffer with a non-zero offset specified. After the contents of the buffer are written to the console, you can see that the data does not start in the buffer until the offset of 10. This is the reason there are the 10 blanks at the beginning of the output.

Finally, you can demonstrate how to write to the stream from a buffer that specifies a non-zero offset. The buffer, myNewDataBuffer, contains the text &quot;Skip Add&quot;. When you specify an offset of 5, only the word &quot;Add&quot; is written to the stream.

</li> <li>On the Build menu, click Build ManagedTestApp.</li> <li> Run the application. The full output from the application is: ---STREAM CONTENTS--- This is unmanaged data.

---STREAM CONTENTS--- This is unmanaged data. This is data added through the StreamWriter class.

---MYBUFFER CONTENTS--- This is unmanaged data. This is data added through the StreamWriter class.

---STREAM CONTENTS--- This is unmanaged data. This is data added through the StreamWriter class. Add Press Enter to continue </li></ol>

back to the top

<div class="references_section">