How can I pass an fstream or equivalent from C# through CLI to an unmanaged C++ DLL?
Rough application outline:
- C# application reads a binary file from a database
- Unmanaged C++ dll is used to "decode" this file and return the information contained therein
- I can modify any of the C# code. The CLI wrapper is the only portion of the C++ side that I can modify.
I'm currently saving the binary file to disk and passing the path of it to the CLI wrapper where it is opened as an fstream. This is fine for test purposes, but won't work for production for obvious reasons.
I've also looked into passing a byte array to the DLL but I was not able to find a way to convert that to an fstream other than with GlobalAlloc, which I would prefer not to use.
Any help or ideas would be appreciated.
Thanks.
You can pass a managed binary array to the C++/CLI DLL. Pin the array. This can then be converted to an STL string object. You can then pass the STL string into an STL stringstream object, which inherits from iostream. Think of stringstream as a .NET MemoryBuffer object. Pass stringstream to your unmanaged C++. That can probably be done in < 10 lines of code. The downside is the data will get copied in memory which is inefficient. For many applications I doubt it would be an issue.
Alternatively, you could write your own class inheriting from stream_buffer that wraps a .NET stream object. (Better to inherit from this instead of iostream, as others propose). That would be the most efficient way because no memory would be needlessly copied but I wouldn't bother doing it if the first method is fast enough.
If your C++ DLL accepts generic iostream objects (instead of just fstreams), create an iostream implementation that wraps System.IO streams and pass it to the DLL. Then the unmanaged side can work directly with the managed stream.
You wont be able to pass in a Memorystream
throught CLI. The best you will be able to do is to pass a 'pointer' (IntPtr) to the byte buffer.
See How can I pass MemoryStream data to unmanaged C++ DLL using P/Invoke? for more details
I was able to get an example working based on this post (PInvoke and IStream).
Basically you need to implement the IStream interface in C#. Then you can pass the custom MemoryStream
as an LPSTREAM
on the C++ side. Here is a code example that takes the stream and gets the size (just a trivial example to show how its done):
C++ LpWin32Dll.h
#ifndef LPWINDLL_H
#define LPWINDLL_H
extern "C" {
__declspec(dllexport) int SizeOfLpStream(LPSTREAM lpStream);
}
#endif
C++ LpWin32Dll.cpp
#include "stdafx.h"
#include <ocidl.h>
#include "LpWin32Dll.h"
// Provides DllMain automatically
[module(dll, name = "LpWin32Dll")];
__declspec(dllexport) int SizeOfLpStream(LPSTREAM lpStream)
{
STATSTG stat_info;
lpStream->Stat(&stat_info, STATFLAG_NONAME);
return stat_info.cbSize.LowPart;
}
C# PInvoke definition
[DllImport("LpWin32Dll.dll", CallingConvention=CallingConvention.StdCall)]
public static extern int SizeOfLpStream(IStream iStream);
C# IStream implementation (Must implement the IStream interface). I've just created a wrapper class for the MemoryStream
class.
[ClassInterface(ClassInterfaceType.AutoDispatch)]
public class IMemoryStream : MemoryStream, IStream {
public IMemoryStream() : base() { }
public IMemoryStream(byte[] data) : base(data) { }
#region IStream Members
public void Clone(out IStream ppstm) { ppstm = null; }
public void Commit(int grfCommitFlags) { }
public void CopyTo(
IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten) { }
public void LockRegion(long libOffset, long cb, int dwLockType) { }
public void Read(byte[] pv, int cb, IntPtr pcbRead)
{
long bytes_read = base.Read(pv, 0, cb);
if (pcbRead != IntPtr.Zero)
Marshal.WriteInt64(pcbRead, bytes_read);
}
public void Revert() { }
public void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition)
{
long pos = base.Seek(dlibMove, (SeekOrigin)dwOrigin);
if (plibNewPosition != IntPtr.Zero)
Marshal.WriteInt64(plibNewPosition, pos);
}
public void SetSize(long libNewSize) { }
public void Stat(
out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg,
int grfStatFlag)
{
pstatstg = new System.Runtime.InteropServices.ComTypes.STATSTG();
pstatstg.cbSize = base.Length;
}
public void UnlockRegion(long libOffset, long cb, int dwLockType) { }
public void Write(byte[] pv, int cb, IntPtr pcbWritten)
{
base.Write(pv, 0, cb);
if (pcbWritten != IntPtr.Zero)
Marshal.WriteInt64(pcbWritten, (long)cb);
}
#endregion
}
C# Use
IMemoryStream ms = new IMemoryStream(new byte[] { 0x45, 0x23, 0x67, 0x34 });
int size = LpTest.SizeOfLpStream(ms);
Your C++/CLI layer can expose a simple interface for the C# side to use, perhaps to pass in byte array objects to stream into the stream library.
Basically the handle/body idiom where the C++/CLI layer wraps the stream and passes an opaque handle back to C# to use.
Create a temporary file. let the operating system allocate a temporary name for you to solve multiple applications (linux can do that, I hope windows can).
Temporary files are acceptable for multi-tool chains in business applications and used to solve just your problem. If the files aren't too big they will stay in cache and if you close and delete it fast enough they won't be even written to disk.