Friday, April 30, 2010

C# implementation of IStream

I'm presently working to expose all the HL7Connect functionality as a COM object, with a focus on making it available to .NET applications - including one that we're writing internally. My experiences with .Net/Com Interop have left me deeply unsatisfied.

One of my biggest struggles has been with IStream. At a COM level, IStream works just fine, and it's been no problems - though it's a bit odd that referring to IStream in my IDL means that a definition of IStream is included in the tlb. But that seems to be normal. More of a problem is what happens when you tlbimp this into C# (either explicitly using Tlbimp, or implicitly by adding a project reference to your visual studio project). What happens is that the imported type library contains a reference to IStream - and each imported reference has the same signature, but in a different namespace. So you'd have to write a different IStream wrapper implementation for each import. What's worse, the interface you get doesn't match the official interface for IStream. I don't know where it comes from.

The solution is to use TlbImp2, and alias your IStream to System.Runtime.InteropServices.ComTypes.IStream. So that's good and useful, but still leaves you with the problem of how to implement an IStream wrapper for stream, and vice versa. There's several useful links out there:
But none of these described quite what I needed, or, when they did, debugging experience suggests that their code is wrong. I don't know why that would be. Anyway, here, for what's worth, is my implementation:

    public class StreamWrapper : IStream
    {
      public StreamWrapper(Stream stream)
      {
        if (stream == null)
          throw new ArgumentNullException("stream", "Can't wrap null stream.");
        this.stream = stream;
      }

      private Stream stream;

      public void Read(byte[] pv, int cb, System.IntPtr pcbRead)
      {
        Marshal.WriteInt32(pcbRead, (Int32)stream.Read(pv, 0, cb));
      }

      public void Seek(long dlibMove, int dwOrigin, System.IntPtr plibNewPosition)
      {
        Marshal.WriteInt32(plibNewPosition, (int) stream.Seek(dlibMove, (SeekOrigin)dwOrigin));
      }

    }
    public class IStreamWrapper : Stream
    {
      IStream stream;

      public IStreamWrapper(IStream stream)
      {
        if (stream == null)
          throw new ArgumentNullException("stream");
        this.stream = stream;
      }

      ~IStreamWrapper()
      {
        Close();
      }

      public override int Read(byte[] buffer, int offset, int count)
      {
        if (offset != 0)
          throw new NotSupportedException("only 0 offset is supported");
        if (buffer.Length < count)
          throw new NotSupportedException("buffer is not large enough");

        IntPtr bytesRead = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)));
        try
        {
          stream.Read(buffer, count, bytesRead);
          return Marshal.ReadInt32(bytesRead);
        }
        finally
        {
          Marshal.FreeCoTaskMem(bytesRead);
        }
      }


      public override void Write(byte[] buffer, int offset, int count)
      {
        if (offset != 0)
          throw new NotSupportedException("only 0 offset is supported");
        stream.Write(buffer, count, IntPtr.Zero);
      }

      public override long Seek(long offset, SeekOrigin origin)
      {
        IntPtr address = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)));
        try
        {
          stream.Seek(offset, (int)origin, address);
          return Marshal.ReadInt32(address);
        }
        finally
        {
          Marshal.FreeCoTaskMem(address);
        }
      }


      public override long Length
      {
        get
        {
          System.Runtime.InteropServices.ComTypes.STATSTG statstg;
          stream.Stat(out statstg, 1 /* STATSFLAG_NONAME*/ );
          return statstg.cbSize;
        }
      }

      public override long Position
      {
        get { return Seek(0, SeekOrigin.Current); }
        set { Seek(value, SeekOrigin.Begin); }
      }


      public override void SetLength(long value)
      {
        stream.SetSize(value);
      }

      public override void Close()
      {
        stream.Commit(0);
        // Marshal.ReleaseComObject(stream);
        stream = null;
        GC.SuppressFinalize(this);
      }

      public override void Flush()
      {
        stream.Commit(0);
      }

      public override bool CanRead
      {
        get { return true; }
      }

      public override bool CanWrite
      {
        get { return true; }
      }

      public override bool CanSeek
      {
        get { return true; }
      }
    }


The big change from the first two links is the change from writeIn64 to writeInt32. The IStream declaration itself has int32 (why, I don't know) and writeInt64 was trashing my stack in the calling application.

This is incomplete - anything missing from here, the body of the method reads:

        throw new Exception("not implemented");

The most notable omissions are the write and seek methods. I'll update this post if when I get around to implementing them.

Btw, it's hard, looking at those few lines of code, to realise how many hours that represents. :-(

2 comments:

  1. Write Method:

    public void Write(byte[] pv, int cb, System.IntPtr pcbWritten)
    {
    int written = Marshal.ReadInt32(pcbWritten);
    stream.Write(pv, 0, written);
    }

    ReplyDelete
  2. Does not work: System.IO.Stream.Read() is allowed to return early and tell it has not read all bytes, but just one or more. IStream::Read() *always* read all the requested bytes unless the end of stream is reached. Different, incompatible contracts.

    ReplyDelete