Microsoft KB Archive/321340

= HOW TO: Wrap a UCOMIStream in a Stream Class =

Article ID: 321340

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 Q321340



IN THIS TASK
 SUMMARY

Create an Unmanaged COM DLL

Create a Managed Application

Define the Class

Implement the Methods of the Class

Create a Managed Assembly from the COM DLL

Use the Class to Work with the Stream

An Alternative Implementation of the Read and Write Methods

 REFERENCES



SUMMARY
This step-by-step article describes how to wrap a UCOMIStream interface in a class that descends 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 a UCOMIStream interface or as Stream objects. Without such a class, you must copy data to and from these different sources and maintain the data between them. This article shows how to use objects of this class in a variety of contexts.

For demonstration purposes, this article shows how to consume and modify an IStream pointer (from the unmanaged COM world) in .NET by using a new class that you develop. First, you generate a very simple unmanaged dynamic-link library (DLL) that returns an IStream pointer to the caller. Then, you create a managed application that references this COM DLL. In the managed application, you develop a new class that descends from the Stream class and wraps a UCOMIStream interface. Through COM Interop, you reference the unmanaged COM DLL and 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 and StreamWriter classes.

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 being complete, this article includes steps to create one:  Open Visual Studio .NET. On the File menu, click New, and then click Project. Under Project Type, click Visual C++ Projects. Under Templates, click ATL Project. For the name of the project, type UnmangedFuncs, and then click OK to create the project. Click Finish in the ATL Wizard dialog box to generate the files for the project.</li> In Class View, right-click the UnmanagedFuncs project, click Add, and then click Add Class.</li> Under Categories, click Visual C++. Under Templates, click ATL Simple Object. Click Open to open the ATL Simple Object Wizard.</li> Type SimpleObj in the Short Name text box, and then click Finish to add the object to the project.</li> In Class View, 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 GetUnmangedData in the Method Name text box.</li> In the Parameter Type combo box, type IStream** .</li> In the Parameter Name text box, type ppData .</li> In the Parameter Attributes section, make sure that only out is selected.</li> Click Add to add this new parameter.</li> Click Finish.</li> In Class View, expand the UnmanagedFuncs project, and then expand the CSimpleObj class. Double-click the GetUnmanagedData method that you just added. This opens the source code for this method.</li>  Change the implementation of this method as follows: 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, you develop a new class that descends from the Stream class and wraps a UCOMIStream interface. Then you use the class to work with streams, such as the one that is returned from the unmanaged COM DLL that you just created.
 * 1) In Solution Explorer, right-click the Solution Object node at the top of the window, click Add, and then click New Project.
 * 2) Under Project Type, click Visual C++ Projects. Under Templates, click Managed C++ Application. In the Name text box, type ManagedTestApp, and then click OK.
 * 3) In the Solution Explorer, right-click the ManagedTestApp project, and then click Set as StartUp Project.
 * 4) On the Build menu, click Build ManagedTestApp.

back to the top

Define the Class
<ol> In Class View, right-click ManagedTestApp, click Add, and then click Add Class.</li> Under Categories, click Visual C++. Under Templates, click Generic C++ Class, and then click Open.</li> <li>In the Generic C++ Class Wizard, type CComStream in the Class Name text box. In the Base Class text box, type Stream, and then click Finish.</li> <li>In Class View, double-click the CComStream class. This opens the ComStream.h file.</li> <li> Change the declaration of this class as follows:
 * 1) pragma once


 * 1) using <mscorlib.dll>

using namespace System; using namespace System::IO; using namespace System::Runtime::InteropServices;

__gc class CComStream : public Stream { private: UCOMIStream* m_pOrigStream;

public: CComStream(UCOMIStream* theOrigStream); ~CComStream(void);

// Stream Methods that require implementation. virtual int Read(unsigned char buffer __gc[], int offset, int count); virtual void Write(unsigned char buffer __gc[], int offset, int count); virtual __int64 Seek(__int64 offset, SeekOrigin origin); virtual void SetLength(__int64 value); virtual void Flush; virtual void Close;

// Stream Properties that require implementation. __property virtual bool get_CanRead; __property virtual bool get_CanWrite; __property virtual bool get_CanSeek; __property virtual __int64 get_Length; __property virtual __int64 get_Position; __property virtual void set_Position(__int64 newPosition); }; Because you are descending from the Stream class, which is a pure virtual class, you must implement several methods. Also, you have added a private member that is a UCOMIStream interface. It is this stream interface that enables you to create instances of objects from the class by using existing interface pointers, which is also demonstrated in this article. </li></ol>

back to the top

Implement the Methods of the Class
<ol> <li>In Solution Explorer, expand the ManagedTestApp project. Then, expand the Source Files folder.</li> <li>Double-click the ComStream.cpp file to open it in the editor.</li> <li> Here you provide the implementation details of the class. The details are described in this article, but for now, change the code in this file as follows:
 * 1) include &quot;StdAfx.h&quot;
 * 2) include &quot;comstream.h&quot;
 * 3) using <mscorlib.dll>

CComStream::CComStream(UCOMIStream* theOrigStream) {   if (theOrigStream != 0) m_pOrigStream = theOrigStream; else throw new ArgumentNullException(&quot;theOrigStream&quot;); }

CComStream::~CComStream(void) {   Close; }

int CComStream::Read(unsigned char buffer __gc[], int offset, int count) {   if (m_pOrigStream == 0) throw new ObjectDisposedException(&quot;m_pOrigStream&quot;);

unsigned int bytesRead = 0;

if (offset != 0) {       unsigned char tmpBuffer __gc[] = new Byte[count]; m_pOrigStream->Read(tmpBuffer, count, &bytesRead); System::Array::Copy(tmpBuffer, 0, buffer, offset,bytesRead); }   else m_pOrigStream->Read(buffer, count, &bytesRead);

return bytesRead; }

void CComStream::Write(unsigned char buffer __gc[], int offset, int count) {   if (m_pOrigStream == 0) throw new ObjectDisposedException(&quot;m_pOrigStream&quot;);

if (offset != 0) {       int iBuffSize = buffer->Length - offset; unsigned char tmpBuffer __gc[] = new Byte[iBuffSize]; System::Array::Copy(buffer, offset, tmpBuffer, 0, iBuffSize); m_pOrigStream->Write(tmpBuffer, iBuffSize, 0); }   else m_pOrigStream->Write(buffer, count, 0); }

__int64 CComStream::Seek(__int64 offset, SeekOrigin origin) {   if (m_pOrigStream == 0) throw new ObjectDisposedException(&quot;m_pOrigStream&quot;);

__int64 curPosition; m_pOrigStream->Seek(offset, origin, &curPosition); return curPosition; }

void CComStream::SetLength(__int64 value) {   if (m_pOrigStream == 0) throw new ObjectDisposedException(&quot;m_pOrigStream&quot;);

m_pOrigStream->SetSize(value); }

void CComStream::Flush {   if (m_pOrigStream == 0) throw new ObjectDisposedException(&quot;m_pOrigStream&quot;);

m_pOrigStream->Commit(0); // STGC_DEFAULT }

void CComStream::Close {   if (m_pOrigStream != 0) {       m_pOrigStream->Commit(0); // STGC_DEFAULT Marshal::ReleaseComObject(m_pOrigStream); m_pOrigStream = 0; GC::SuppressFinalize(this); } }

bool CComStream::get_CanRead {   return true; }

bool CComStream::get_CanWrite {   return true; }

bool CComStream::get_CanSeek {   return true; }

__int64 CComStream::get_Length {   if (m_pOrigStream == 0) throw new ObjectDisposedException(&quot;m_pOrigStream&quot;);

STATSTG statstg; m_pOrigStream->Stat(&statstg, 1 /* STATFLAG_NONAME */); return statstg.cbSize; }

__int64 CComStream::get_Position {   return Seek(0, SeekOrigin::Current); }

void CComStream::set_Position(__int64 newPosition) {   Seek(newPosition, SeekOrigin::Begin); }

To examine this code, look first at the constructor and destructor of the class. To create an instance of an object from this class, you must pass in a UCOMIStream interface. Later, you will see how to pass an unmanaged IStream interface to create an instance of an object of this class. If a null pointer is passed in, an exception is generated. Without this interface, the class does not function properly.

The destructor calls Stream::Close to close out the stream. This also makes sure that your data is flushed when the object is closed.

Before you get into the details of the Read method, you must understand that the UCOMIStream::Read and Stream::Read methods are not exactly compatible. The Stream::Read method provides for the idea of reading data from a stream into a buffer at an offset other than zero in the buffer. However, the UCOMIStream::Read method does not support this idea, which you can see because offset is not a parameter that it expects. It assumes that it will be reading data from the stream and putting it in the buffer at offset zero. Now that you are aware of this difference, understanding this implementation of Read is easier.

The first thing that you do is verify that your UCOMIStream interface pointer is valid and generate an exception if it is not. You will see this same test made in any method that uses the UCOMIStream interface. Next, in the event where offset is not zero, you start by creating a temporary buffer that is the size indicated by count (that is, the number of bytes to read). Then, you call UCOMIStream::Read to read count bytes into the temporary buffer. Finally, you copy the number of bytes that you read from the temporary buffer to the offset in the buffer that will be returned to the caller. Alternatively, in the case where the offset that is specified by the caller is zero, you call UCOMIStream::Read to perform the operation.

The details of the Write method are similar to those of the Read method. Again, you must handle the scenario where the offset that is specified is not zero.

In the implementation of the Seek method, you call the UCOMIStream::Seek method, passing it the offset and origin that was passed to you. The third parameter contains the position in the stream after you perform the seek operation, and is ultimately the value that 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 you pass 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, it does require an implementation for your class to be fully functional. The first thing that you do is commit the stream to storage. Next, you use the static method ReleaseComObject in the Marshal class to release your reference to the interface that is provided in your constructor. Without this, the Runtime Callable Wrapper (RCW) maintains a positive reference count to the COM object and does not release, resulting in the unnecessary usage of memory and resources. The last thing that you do is call the static method SuppressFinalize in the GC class. This prevents the garbage collector from calling your destructor again when it claims the memory for objects that are instantiated from your class. For more information about GC::SuppressFinalize and Marshal::ReleaseComObject methods, see the &quot;References&quot; section.

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

The Length property, which is a read-only property, has to do some additional work to get the result that you want. The UCOMIStream interface does not have a Length method that you can use. Instead, it provides a Stat method that can be used to provide the value that you are looking for, 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

Create a Managed Assembly from the COM DLL
Before you can reference your COM DLL in your managed application, you must first create a managed assembly from it. The tool that you use to do this is the Tlbimp.exe utility. This creates an RCW for your DLL so that you can use it in a managed application. For more information about what an RCW is, see the &quot;References&quot; section.
 * 1) Open a .NET command prompt. To do this, click Start, click Programs, click Microsoft Visual Studio .NET, click Visual Studio .NET Tools, and then click Visual Studio .NET Command Prompt.
 * 2) Change directories to the location of the COM DLL that you generated previously. If you created a debug build, it will be in the \UnmanagedFuncs\Debug folder. Otherwise, locate \UnmanagedFuncs\Release.
 * 3) At the command prompt, type tlbimp UnmanagedFuncs.dll /out:unmanagedassem.dll /namespace:UnmanagedStreams to generate the Unmanagedassem.dll file.
 * 4) Copy Unmanagedassem.dll to \ManagedTestApp\Debug.

back to the top

Use the Class to Work with the Stream
This section shows how to use objects of the class in a variety of contexts. You reference your COM DLL by using the managed assembly that you created previously and use the IStream interface from the GetUnmanagedData function to create an instance of an object from your class. Then, you work with this stream by using various managed functions. <ol> <li>In Solution Explorer, expand the ManagedTestApp project. Then, expand the Source Files folder.</li> <li>Double-click the ManagedTestApp.cpp file to open it in the editor.</li> <li> Change the code in this file as follows: // This is the main project file for VC++ application project // generated using an Application Wizard. using namespace System; using namespace UnmanagedStreams; // The namespace must be exactly as it was specified // when you created unmanagedassem.dll.
 * 1) include &quot;stdafx.h&quot;
 * 2) include <tchar.h>
 * 3) include &quot;ComStream.h&quot;
 * 4) using <mscorlib.dll>
 * 5) using <.\debug\unmanagedassem.dll>

int _tmain(void) {   // Using COM InterOp, instantiate some unmanaged object that uses/returns IStream. UnmanagedStreams::CSimpleObj* myCOMObject = new CSimpleObjClass;

// Get an IStream pointer from the object. IStream* pStrm; myCOMObject->GetUnmanagedData(&pStrm);

// Create an instance of your stream class, using the IStream interface we already have. CComStream* pComStream = new CComStream(__try_cast<UCOMIStream*>(pStrm));

// Display the contents of the stream using the StreamReader class. pComStream->Seek(0, SeekOrigin::Begin); StreamReader *sr = new StreamReader(__try_cast<Stream*>(pComStream)); Console::WriteLine(S&quot;---STREAM CONTENTS---&quot;); Console::WriteLine(sr->ReadToEnd);

// Add some data to the stream using the StreamWriter class. StreamWriter* sw = new StreamWriter(__try_cast<Stream*>(pComStream)); sw->WriteLine(S&quot;&quot;); sw->WriteLine(S&quot;This is data added through the StreamWriter class.&quot;); sw->Flush;

// Display the contents of the stream using the StreamReader class. pComStream->Seek(0, SeekOrigin::Begin); Console::WriteLine; Console::WriteLine(S&quot;---STREAM CONTENTS---&quot;); Console::WriteLine(sr->ReadToEnd);

// Try reading from the stream into a buffer with an offset other than 0. unsigned char myBuffer __gc[] = new Byte[100]; pComStream->Seek(0, SeekOrigin::Begin); pComStream->Read(myBuffer, 10, myBuffer->Length); Console::WriteLine(S&quot;---MYBUFFER CONTENTS---&quot;); for (int i = 0; i < myBuffer->Length; i++) Console::Write(Convert::ToChar(myBuffer->GetValue(i))); Console::WriteLine;

// Try writing to the stream using a buffer offset that is not 0. unsigned char myNewDataBuffer __gc[] = {'S', 'k', 'i', 'p', ' ', 'A', 'd', 'd'}; //                                                               ^ This is offset 5 in the buffer. pComStream->Write(myNewDataBuffer, 5, myNewDataBuffer->Length);

// Display the contents of the stream using the StreamReader class. pComStream->Seek(0, SeekOrigin::Begin); Console::WriteLine; Console::WriteLine(S&quot;---STREAM CONTENTS---&quot;); Console::WriteLine(sr->ReadToEnd);

return 0;

} You start with a COM Interop call to an unmanaged COM object that returns an IStream interface. Using this interface, you create an instance of an object from your class. Next, you display the contents of the stream as it was returned to you from the call to GetStreamData. The StreamReader class is used to read the contents of the stream and write them out to the console. Next, you use the StreamWriter class to add data to the stream. Finally, you use your stream reader to again display the full contents of the stream.

Next, you demonstrate reading from the stream into a buffer with a nonzero offset specified. When you write the contents of the buffer to the console, you can see that the data does not start in the buffer until you get to offset 10, which is why you see the 10 blanks at the beginning of the output.

Finally, you demonstrate writing to the stream from a buffer where you have specified a nonzero 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 any key to continue </li></ol>

back to the top

An Alternative Implementation of the Read and Write Methods
The current implementation of CComStream is portable, meaning that you can use this class in any other application without making any changes to the code. In this implementation, the Read and Write methods create a temporary buffer to bridge the gap between the Stream and UCOMIStreamRead and Write methods. However, another way to implement these methods exists where the temporary buffer can be avoided. In this case, you use the IStream pointer to reference the original stream. Because the IStreamRead and Write methods expect a pointer to the buffer, you can change where these pointers are actually pointing in the buffer. Implementing these methods in this way requires a few simple changes: <ol> <li> Add a reference to your assembly in the CCompStream.cpp file. This permits you to access the original stream. Change the top of the CComStream.cpp file as follows: using namespace UnmanagedStreams;               // Add this line. </li> <li> Change the implementation of the Read and Write methods as follows: int CComStream::Read(unsigned char buffer __gc[], int offset, int count) {   if (m_pOrigStream == 0) throw new ObjectDisposedException(&quot;m_pOrigStream&quot;);
 * 1) include &quot;StdAfx.h&quot;
 * 2) include &quot;comstream.h&quot;
 * 3) using <mscorlib.dll>
 * 4) using <.\debug\unmanagedassem.dll>             // Add this line.

unsigned int bytesRead = 0;

if (offset != 0) {       IStream *pStream = __try_cast<IStream*>(m_pOrigStream); unsigned char __pin *pBuffer = &buffer[offset]; pStream->RemoteRead(pBuffer, count, &bytesRead); }   else m_pOrigStream->Read(buffer, count, &bytesRead);

return bytesRead; }

void CComStream::Write(unsigned char buffer __gc[], int offset, int count) {   if (m_pOrigStream == 0) throw new ObjectDisposedException(&quot;m_pOrigStream&quot;);

if (offset != 0) {       IStream *pStream = __try_cast<IStream*>(m_pOrigStream); unsigned char __pin *pBuffer = &buffer[offset]; pStream->RemoteWrite(pBuffer, count, 0); }   else m_pOrigStream->Write(buffer, count, 0); } The only changes are in the code to handle offsets that are nonzero. Everything else has remained the same. In both cases, you start by casting your internal stream to an IStream pointer. This gives you an interface pointer to the original stream that you created in your unmanaged COM DLL. Next, you create a pinned pointer to the buffer that points to the specified offset in the buffer. The reason that you do this is because the IStream::Read method accepts a generic pointer (void*) for the buffer parameter. Remember, the UCOMIStream::Read method does not allow you to do this. It is this pointer that you then pass to the Read and Write methods.

You probably noticed that you are calling RemoteRead and RemoteWrite instead of Read and Write. This is the result of a technique named &quot;method aliasing&quot; that is used when the type library is generated for the COM DLL that you are using. It is beyond the scope of this article to explain these details. For now, just be aware that the following IStream methods are renamed accordingly:

<ul> <li>Read is referred to as RemoteRead.</li> <li>Write is referred to as RemoteWrite.</li> <li>CopyTo is referred to as RemoteCopyTo.</li> <li>Seek is referred to as RemoteSeek.</li></ul> </li></ol>

So, which way is better? That depends on your application. The first implementation is more portable, but requires you to create temporary buffers to implement these two methods.

The second implementation goes directly to the source of the stream, bypassing the need for temporary buffers. So, it is more efficient in its implementation. However, you lose portability because you must reference the RCW in the source.

Consider these points when you decide which implementation works best for you.

back to the top

<div class="references_section">