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.
- 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.
- When the ATL Wizard dialog box appears, click Finish to generate the
files for the project.
- 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.
- 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.
- Type SimpleObj in the Short Name field and then click Finish to add the object to
the project.
- 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.
- In the Add Method Wizard, type
GetUnmanagedData in the Method Name field.
- In the Parameter Type combo box, type IStream**. In the Parameter Name field, type ppData.
- Click out in the "Parameter Attributes"
section. Make sure that this is the only check box that is selected.
- Click Add to add this new parameter and
then click Finish.
- 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. - 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;
} - 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.
- Open Visual Studio .NET.
- On the File menu, click New and then click
Project to display the New Project dialog box.
- Under Project Type, click to select Visual Basic Projects. Under Templates, click Console Application. In the Name field, type ManagedTestApp and then click OK.
- In Solution Explorer, right-click the
ManagedTestApp project and then click Set as StartUp
Project.
- In Solution Explorer, right-click the
ManagedTestApp project and then click Add
Reference.
- In the Add Reference dialog box, click to select the COM tab.
- 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.
- In Solution Explorer, expand the References node and then
verify that UnmanagedFuncs has been added.
- In Solution Explorer, right-click
ManagedTestApp, click Add, and then click
Add Class.
- Under Categories, click Local Project
Items. Under Templates, click Class. In the Name
field, type ComStream.vb and then click
Open.
- 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.
- In Solution Explorer, expand the
ManagedTestApp project.
- Double-click the ComStream.vb file to
open the file in the editor.
- 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.
- In Solution Explorer, expand the
ManagedTestApp project.
- Double-click the Module1.vb file to open
the file in the editor.
- 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.
- On the Build menu, click Build
ManagedTestApp.
- 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
REFERENCES For more information,
visit the following Microsoft Developer Network (MSDN) Web sites: For more information about COM Interop, see the following book:
back to the top
Modification Type: | Major | Last Reviewed: | 1/19/2004 |
---|
Keywords: | kbHOWTOmaster KB321695 kbAudDeveloper |
---|
|