HOW TO: Wrap a UCOMIStream in a Stream Class (321340)
The information in this article applies to:
- Microsoft .NET Framework 1.0
- Microsoft .NET Framework 1.1
This article was previously published under Q321340 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.
- In Class View, right-click the UnmanagedFuncs project, click Add, and then click Add Class.
- Under Categories, click Visual C++. Under Templates, click ATL Simple Object. Click Open to open the ATL Simple Object Wizard.
- Type SimpleObj in the Short Name text box, and then click Finish to add the object to the project.
- 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.
- In the Add Method Wizard, type
GetUnmangedData in the Method Name text box.
- In the Parameter Type combo box, type IStream**.
- In the Parameter Name text box, type ppData.
- In the Parameter Attributes section, make sure that only out is selected.
- Click Add to add this new parameter.
- Click Finish.
- 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.
- 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("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, 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.
- In Solution Explorer, right-click the Solution Object node at the top of the window, click Add, and then click New Project.
- Under Project Type, click Visual C++ Projects. Under Templates, click Managed C++ Application. In the Name text box, type ManagedTestApp, and then
click OK.
- In the Solution Explorer, right-click the ManagedTestApp project, and then click Set as StartUp Project.
- On the Build menu, click Build ManagedTestApp.
back to the top
Define the Class- In Class View, right-click ManagedTestApp, click Add, and then click Add Class.
- Under Categories, click Visual C++. Under Templates, click Generic C++ Class, and then click Open.
- 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.
- In Class View, double-click the CComStream class. This opens the ComStream.h file.
- Change the declaration of this class as follows:
#pragma once
#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.
back to the top
Implement the Methods of the Class- In Solution Explorer, expand the ManagedTestApp project. Then, expand the Source Files folder.
- Double-click the ComStream.cpp file to open it in the editor.
- 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:
#include "StdAfx.h"
#include "comstream.h"
#using <mscorlib.dll>
CComStream::CComStream(UCOMIStream* theOrigStream)
{
if (theOrigStream != 0)
m_pOrigStream = theOrigStream;
else
throw new ArgumentNullException("theOrigStream");
}
CComStream::~CComStream(void)
{
Close();
}
int CComStream::Read(unsigned char buffer __gc[], int offset, int count)
{
if (m_pOrigStream == 0)
throw new ObjectDisposedException("m_pOrigStream");
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("m_pOrigStream");
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("m_pOrigStream");
__int64 curPosition;
m_pOrigStream->Seek(offset, origin, &curPosition);
return curPosition;
}
void CComStream::SetLength(__int64 value)
{
if (m_pOrigStream == 0)
throw new ObjectDisposedException("m_pOrigStream");
m_pOrigStream->SetSize(value);
}
void CComStream::Flush()
{
if (m_pOrigStream == 0)
throw new ObjectDisposedException("m_pOrigStream");
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("m_pOrigStream");
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
"tagSTGC", 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 "References" 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.
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 "References" section.
- 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.
- 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.
- At the command prompt, type tlbimp
UnmanagedFuncs.dll /out:unmanagedassem.dll
/namespace:UnmanagedStreams to generate the Unmanagedassem.dll
file.
- 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.
- In Solution Explorer, expand the ManagedTestApp project. Then, expand the Source Files folder.
- Double-click the ManagedTestApp.cpp file to open it in the editor.
- Change the code in this file as follows:
// This is the main project file for VC++ application project
// generated using an Application Wizard.
#include "stdafx.h"
#include <tchar.h>
#include "ComStream.h"
#using <mscorlib.dll>
#using <.\debug\unmanagedassem.dll>
using namespace System;
using namespace UnmanagedStreams; // The namespace must be exactly as it was specified
// when you created 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"---STREAM CONTENTS---");
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"");
sw->WriteLine(S"This is data added through the StreamWriter class.");
sw->Flush();
// Display the contents of the stream using the StreamReader class.
pComStream->Seek(0, SeekOrigin::Begin);
Console::WriteLine();
Console::WriteLine(S"---STREAM CONTENTS---");
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"---MYBUFFER CONTENTS---");
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"---STREAM CONTENTS---");
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 "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 any key to continue
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:
- 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:
#include "StdAfx.h"
#include "comstream.h"
#using <mscorlib.dll>
#using <.\debug\unmanagedassem.dll> // Add this line.
using namespace UnmanagedStreams; // Add this line.
- 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("m_pOrigStream");
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("m_pOrigStream");
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 "method aliasing" 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:
- Read() is referred to as RemoteRead().
- Write() is referred to as RemoteWrite().
- CopyTo() is referred to as RemoteCopyTo().
- Seek() is referred to as RemoteSeek().
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
REFERENCES For more information,
visit the following Microsoft Developer Network (MSDN) Web sites: For more information about COM Interop, refer to the following
book:
back to the top
Modification Type: | Major | Last Reviewed: | 1/19/2004 |
---|
Keywords: | kbhowto kbinterop KB321340 kbAudDeveloper |
---|
|