HOW TO: Wrap a UCOMIStream in a Stream Class in Visual Basic .NET (321695)



The information in this article applies to:

  • Microsoft .NET Framework 1.0
  • Microsoft .NET Framework 1.1

This article was previously published under Q321695

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.
  1. Open Visual Studio .NET.
  2. On the File menu, click New and then click Project to display the New Project dialog box.
  3. In the Project Type window, click Visual C++ Projects. In the Templates window, click ATL Project.
  4. For the name of the project, type UnmanagedFuncs and then click OK to create the project.
  5. When the ATL Wizard dialog box appears, click Finish to generate the files for the project.
  6. 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.
  7. 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.
  8. Type SimpleObj in the Short Name field and then click Finish to add the object to the project.
  9. 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.
  10. In the Add Method Wizard, type GetUnmanagedData in the Method Name field.
  11. In the Parameter Type combo box, type IStream**. In the Parameter Name field, type ppData.
  12. Click out in the "Parameter Attributes" section. Make sure that this is the only check box that is selected.
  13. Click Add to add this new parameter and then click Finish.
  14. 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.
  15. 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("This is unmanaged data.", 24, &lBytesWritten);
    	if (FAILED(hr))
    		return hr;
    
    	return S_OK;
    
    }
  16. On the Build menu, click Build UnmanagedFuncs.
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 "Selected Components" 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.
  1. In Solution Explorer, expand the ManagedTestApp project.
  2. Double-click the ComStream.vb file to open the file in the editor.
  3. 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("theStream")
          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("theStream")
          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("theStream")
          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("theStream")
          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("theStream")
             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("theStream")
          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("theStream")
          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 "tagSTGC", 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 "References" 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.
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.
  1. In Solution Explorer, expand the ManagedTestApp project.
  2. Double-click the Module1.vb file to open the file in the editor.
  3. 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("---STREAM CONTENTS---")
            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("This is data added by using the StreamWriter class.")
            sw.Flush()
    
            'Display the contents of the stream using the StreamReader class.
            StreamObj.Seek(0, SeekOrigin.Begin)
            Console.WriteLine()
            Console.WriteLine("---STREAM CONTENTS---")
            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("---MYBUFFER CONTENTS---")
            Console.WriteLine(encoding.GetString(myBuffer))
    
            'Try writing to the stream using a buffer offset that is not 0.
            Dim myNewDataBuffer() As Char = {"S", "k", "i", "p", " ", "A", "d", "d"}
            '                                                  ^ 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("---STREAM CONTENTS---")
            Console.WriteLine(sr.ReadToEnd())
            Console.Write("Press Enter to continue")
            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 "Skip Add". When you specify an offset of 5, only the word "Add" is written to the stream.

  4. On the Build menu, click Build ManagedTestApp.
  5. 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
    					
back to the top

Modification Type:MajorLast Reviewed:1/19/2004
Keywords:kbHOWTOmaster KB321695 kbAudDeveloper