How to? Pass C# Stream object to unmanaged COM method that expects a pointer?

T

TJ

I need to be able to pass a pointer to a (managed code) Stream object to a
COM method so it can serialize its data into the stream.

I have a C# application that makes uses of Randolf Duke's MimeSniffer COM
component (http://www.codeproject.com/internet/mimesniffer.asp). My
application (pop3ImageGrabber) uses MimeSniffer to identify each image
attachment in emails that have been collected from a POP3 mailbox.

MimeSniffer gives the option of saving each attachment to a stream. It
provides two methods for this:

interface IMimeBody : IDispatch {
HRESULT ExportAsFile([in] BSTR Path, [out,retval] VARIANT_BOOL*
pbvarResult);
HRESULT Export([in] LPUNKNOWN pStream, [out,retval] VARIANT_BOOL*
pbvarResult);
}

I am currently saving to a file on disk using ExportAsFile(), then loading
the saved image back from disk using:

attachment.ExportAsFile(path + "tmp_" + attachFileName); // save the image
to disk
Bitmap image = new Bitmap(path + "tmp_" + attachFileName); // load it into a
bitmap for processing

However, for performance reasons I'd like to be able to simply do this:

MemoryStream ms = new MemoryStream();
bool err = attachment.Export(ms);
image = new Bitmap(ms);

But this causes the run-time error:

"An unhandled exception of type 'System.ArgumentException' occurred in
system.drawing.dll - Invalid parameter used"

when the call to Bitmap(ms) is made because the MemoryStream object wasn't
written to by attachment.Export() - size remains zero.

I tried various permutations of marshaling including:

MemoryStream ms = new MemoryStream();
IntPtr pStream = Marshal.GetIUnknownForObject(ms);
attachment.Export(pStream);
Marshal.Release(pStream);
image = new Bitmap(ms);

This doesn't work either. I've Googled all over without finding a solution.

You can see from the C++ source for the COM methods Export() and
ExportAsFile() quoted below that the method expects a pointer to an IStream.

STDMETHODIMP CMimeBody::Export(LPUNKNOWN pStream, VARIANT_BOOL *pbvarResult)
{
CYYSType& type = GetField(_T("Content-Transfer-Encoding"));
*pbvarResult = VARIANT_FALSE;
if (type.GetType() != CYYSType::type_Long)
type = (long)CMimeDecoder::mechanism_unknown;
if (type.IsValid())
{
if (type.GetType() == CYYSType::type_Long)
{
CYYSType* bContentIsRaw = NULL;
CYYSType& body = GetBody(bContentIsRaw);
if (body.GetType() == CYYSType::type_Bulk)
{
IStreamPtr pDest;
if (SUCCEEDED(pStream->QueryInterface(&pDest)))
{
long encoding = bContentIsRaw != NULL && bContentIsRaw->m_nVal ?
CMimeDecoder::mechanism_unknown : (long)type;
CMIMECode* pCoder = CMIMECode::GetCoderByTransferType(encoding);
if (pCoder != NULL)
{
SEEK_TO_BEGIN(body.m_streamVal)
pCoder->Decode(body.m_streamVal, pDest);
*pbvarResult = VARIANT_TRUE;
delete pCoder;
}
}
}
}
}
return S_OK;
}

STDMETHODIMP CMimeBody::ExportAsFile(BSTR Path, VARIANT_BOOL* pbvarResult)
{
USES_CONVERSION;
*pbvarResult = VARIANT_FALSE;
HANDLE hFile = ::CreateFile(OLE2CT(Path), GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, 0, NULL);
if (hFile != INVALID_HANDLE_VALUE)
{
CFileSystemStream* pStream = new CFileSystemStream(hFile);
Export(pStream, pbvarResult);
pStream->Release();
}
return S_OK;
}

class CFileSystemStream : public IStream
{//...}

This managed code business is driving me nuts - it seems like its casually
put out of bounds thousands of useful COM objects simply to be able to have
a nifty garabage collector!

TJ.
 
O

Oliver Sturm

TJ wrote:

You can see from the C++ source for the COM methods Export() and
ExportAsFile() quoted below that the method expects a pointer to an IStream.

That's the important information here! Now what you need to do is create
a wrapper class for the .NET stream that implements the COM IStream
interface.

I have once written a blog article that contains part of such a wrapper
implementation. Only part, because the task at hand required me only to
implement the Read method, not the Write method. Still, you should be
able to easily use this code and implement the Write method
additionally. You can find the article here (the code for the wrapper
class is about half way down):

http://www.sturmnet.org/blog/archives/2005/03/03/cds-csharp-extractor/



Oliver Sturm
 
T

TJ

Many thanks for that, Oliver. I was obstinately resisting having to create
wrapper classes! When I first tried your solution I was confused that
VisualStudio 2003 didn't know about
"System.Runtime.InteropServices.ComTypes.IStream" but after a quick MSDN
check I realised its only in VisualStudio 2005 beta - so I switched the
project into that environment and made excellent progress.

Because of the way my code uses the MemoryStream I had to inherit from two
base classes: MemoryStream *and* IStream.

// transfer the attachment image to a bitmap
IMemoryStream ms = new IMemoryStream();
attachment.Export(ms); // call IMimeBody.Export(IStream)
image = new Bitmap((Stream)ms);

For others that need to do something similar, here's my code. Notice that I
have commented out the Marshal.WriteInt64() calls because of IllegalAccess
exceptions that I think are caused by insecure code:

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Drawing;
using System.Drawing.Imaging;
using net.tjworld.utils;

namespace net.tjworld {
/// <summary>
/// Can't do multiple inheritence so need to wrap MemoryStream
// </summary>
class MyMemoryStream : MemoryStream {
public MyMemoryStream() : base() { } // call superclass constructor - in
C# should happen by default anyway
}

/// <summary>
/// COM IStream wrapper for a MemoryStream. Inherit from MyMemoryStream,
implement IStream
/// </summary>
class IMemoryStream : MyMemoryStream, IStream {
public IMemoryStream() : base() { }
public void Clone(out System.Runtime.InteropServices.ComTypes.IStream
ppstm) { ppstm = null; }

public void Read(byte[] pv, int cb, System.IntPtr pcbRead) {
int bytesRead = base.Read(pv, 0, cb);
// Marshal.WriteInt64(pcbRead, (Int64)bytesRead);
}
public void Write(byte[] pv, int cb, System.IntPtr pcbWritten) {
base.Write(pv, 0, cb);
// Marshal.WriteInt64(pcbWritten, cb);
}
public void Seek(long dlibMove, int dwOrigin, System.IntPtr
plibNewPosition) {
int pos = base.Seek(dlibMove, (SeekOrigin)dwOrigin);
// Marshal.WriteInt64(plibNewPosition, (Int64)pos);
}
public void SetSize(long libNewSize) { }
public void CopyTo(System.Runtime.InteropServices.ComTypes.IStream pstm,
long cb, System.IntPtr pcbRead, System.IntPtr pcbWritten) { }
public void Commit(int grfCommitFlags) { }
public void LockRegion(long libOffset, long cb, int dwLockType) { }
public void Revert() { }
public void UnlockRegion(long libOffset, long cb, int dwLockType) { }
public void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG
pstatstg, int grfStatFlag) {
pstatstg = new System.Runtime.InteropServices.ComTypes.STATSTG();
}
}
// other members and classes ...
}
 
O

Oliver Sturm

TJ said:
Many thanks for that, Oliver. I was obstinately resisting having to create
wrapper classes! When I first tried your solution I was confused that
VisualStudio 2003 didn't know about
"System.Runtime.InteropServices.ComTypes.IStream" but after a quick MSDN
check I realised its only in VisualStudio 2005 beta - so I switched the
project into that environment and made excellent progress.

Oops :) Sorry about that. You'd have to define the interface yourself
in 2003 if it's not available by default.

It should look something like this:

[ComImport, Guid("0000000c-0000-0000-C000-000000000046"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IStream
{
void Read([Out, MarshalAs(UnmanagedType.LPArray,
SizeParamIndex=1)] byte[] pv, int cb, IntPtr pcbRead);
void Write([MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)]
byte[] pv, int cb, IntPtr pcbWritten);
void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition);
void SetSize(long libNewSize);
void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr
pcbWritten);
void Commit(int grfCommitFlags);
void Revert();
void LockRegion(long libOffset, long cb, int dwLockType);
void UnlockRegion(long libOffset, long cb, int dwLockType);
void Stat(out STATSTG pstatstg, int grfStatFlag);
void Clone(out IStream ppstm);
}




Oliver Sturm
 
T

TJ

One last 'little' helping hint...

As I said before, I'm using MimeSniffer by Randolf Duke.At one point
MimeSniffer calls my IMemoryStream.Write(byte[] pv,int cb,System.IntPtr
pcbWritten) using the following C++ code construct:
pDest->Write(pBuf, dwRead, NULL);

from within its MimeBody.Export() method.

Notice that the parameter pcbWritten is NULL. This is what causes the
protected memory exception I mentioned in an earlier article.

I tried testing pcbWritten for equality to null:

if(pcbWritten != null) Marshal.WriteInt64(pcbWritten, (Int64)cb);

but of course the System.IntPtr isn't null.


So how can we test for a null value pointed to by the System.IntPtr?
 
T

TJ

Thanks for that solution Willy! I had been staring at IntrPtr.Zero in the
debugger but hadn't realised it's significance, and didn't find anything in
MSDN that made me realise.

For those in the future wanting to pass pointers to unmanaged code, and to
make calls from unmanaged COM objects into managed code, especially for
Streams, here's my coding. Like the base code provided by Oliver Sturm I've
only implemented the methods I need for the current project, so you might
need to add functionality. If you do please post it back here for the
community to learn from.

C# source code - you may well want to change the namespace to your own.

---- IMemoryStream.cs ----

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace net.tjworld.utils
{
// For framework 1.1 define the COM IStream interface (framework 2.x has it
built in)
// Thanks to Oliver Sturm for this
[ComImport, Guid("0000000c-0000-0000-C000-000000000046"), InterfaceType
(ComInterfaceType.InterfaceIsIUnknown)]
public interface IStream {
void Read([Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)] byte[]
pv, int cb, IntPtr pcbRead);
void Write([MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)] byte[] pv,
int cb, IntPtr pcbWritten);
void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition);
void SetSize(long libNewSize);
void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten);
void Commit(int grfCommitFlags);
void Revert();
void LockRegion(long libOffset, long cb, int dwLockType);
void UnlockRegion(long libOffset, long cb, int dwLockType);
void Stat(out STATSTG pstatstg, int grfStatFlag);
void Clone(out IStream ppstm);
}
// wrap the Stream prior to extending it
public class MyMemoryStream : MemoryStream {
public MyMemoryStream() : base() { }
}
/// <summary>
/// COM IStream wrapper for a MemoryStream.
/// Thanks to Willy Denoyette for the if(System.IntPr != IntPtr.Zero) test
for a NULL parameter via COM
/// CLR will make the class implement the IDispatch COM interface
/// so COM objects can make calls to IMemoryStream methods
/// </summary>
[ClassInterface(ClassInterfaceType.AutoDispatch)]
public class IMemoryStream : MyMemoryStream, IStream {
public IMemoryStream() : base() { }
// convenience method for writing Strings to the stream
public void Write(string s) {
System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
byte[] pv = encoding.GetBytes(s);
Write(pv, 0, pv.GetLength(0));
}
// Implementation of the IStream interface
public void Clone(out IStream ppstm) {
ppstm = null;
}
public void Read(byte[] pv, int cb, System.IntPtr pcbRead) {
long bytesRead = Read(pv, 0, cb);
if(pcbRead != IntPtr.Zero) Marshal.WriteInt64(pcbRead, bytesRead); //
only set actual bytes read if pointer isn't NULL
}
public void Write(byte[] pv, int cb, System.IntPtr pcbWritten) {
Write(pv, 0, cb);
if(pcbWritten != IntPtr.Zero) Marshal.WriteInt64(pcbWritten, (Int64)cb);
// only set actual bytes written if pointer isn't NULL
}
public void Seek(long dlibMove, int dwOrigin, System.IntPtr
plibNewPosition) {
long pos = base.Seek(dlibMove, (SeekOrigin)dwOrigin);
if(plibNewPosition != IntPtr.Zero) Marshal.WriteInt64(plibNewPosition,
pos); // only set actual bytes written if pointer isn't NULL
}
public void SetSize(long libNewSize) { }
public void CopyTo(IStream pstm, long cb, System.IntPtr pcbRead,
System.IntPtr pcbWritten) { }
public void Commit(int grfCommitFlags) { }
public void LockRegion(long libOffset, long cb, int dwLockType) { }
public void Revert() { }
public void UnlockRegion(long libOffset, long cb, int dwLockType) { }
public void Stat(out STATSTG pstatstg, int grfStatFlag) {
pstatstg = new STATSTG();
}
}
}
 
T

TJ

Oops! would help to remove redundant code that hung around from my testing
phase!

Remove this completely:

// wrap the Stream prior to extending it
public class MyMemoryStream : MemoryStream {
public MyMemoryStream() : base() { }
}

Alter this:

public class IMemoryStream : MyMemoryStream, IStream {

to read:

public class IMemoryStream : MemoryStream, IStream {

I've written this up at http://tjworld.net/software/IMemoryStream/ and I
will post additions and improvements to the code there.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top