accessing COM from C# with an IStream** parameter

R

Ralf

Hallo,

I'm trying to call a COM function from C#.
The function has a parameter from the type array of IStream* (IStream**).
The COM component (out-proc-server (.exe)) has the following code:

(C++)

STDMETHODIMP CIntf::SendFiles(int noOfStreams, IUnknown** dataStreams)
{
for(int i = 0; i < noOfStreams; i++)
{
if(*(dataStreams + i))
{
IStream* iStream;
hr = (*(dataStreams + i))->QueryInterface(IID_IStream,
(void**)&iStream);
... ...
}
}
}

I can't change the code of the COM component.
If I'm calling the function from C# I get an E_NOINTERFACE error from
QueryInterface in the COM component.
Does anyone has an idea about the problem in my C# code?

I asked the same in the german newsgroup, but I got no answer. I hope you
are better here.

Best reguards
Ralf Beckers

(C#)

public void SendFiles(string file)
{

FileStream fs = new FileStream(file, FileMode.Open);
StreamReader sr = new StreamReader(fs, Encoding.Default);
string trc = sr.ReadToEnd();
sr.Close();
fs.Close();

byte[] data = Encoding.Default.GetBytes(trc);

IStorage storage;
ComTypes.IStream stream;

int err = StgCreateDocfile(null,
STGM.CREATE | STGM.READWRITE | STGM.TRANSACTED |
STGM.SHARE_EXCLUSIVE | STGM.DELETEONRELEASE,
0, out storage);

storage.CreateStream("TRTFiles.trt", (uint)(STGM.READWRITE |
STGM.SHARE_EXCLUSIVE), 0,0, out stream);
stream.Write(data, data.Length, IntPtr.Zero);
stream.Commit((int)STGC.OVERWRITE);

storage.Commit((int)STGC.OVERWRITE);


IntPtr ptr = Marshal.GetIUnknownForObject(stream);
IntPtr[] arr = { ptr };
IntPtr buffer = Marshal.AllocCoTaskMem( Marshal.SizeOf( typeof(IntPtr ))
* arr.Length );
Marshal.Copy(arr, 0, buffer, arr.Length );

object obj = buffer;
this.intf.SendFiles(1, ref obj);
Marshal.FreeCoTaskMem((IntPtr)buffer);
}


[DllImport("ole32.dll")]
static extern int StgCreateDocfile([MarshalAs(UnmanagedType.LPWStr)]string
pwcsName, STGM grfMode, uint reserved, out IStorage ppstgOpen);

[Flags]
public enum CSMFlags : short
{
NONE = 0,
EVENT_EN = 0x0100,
POLLED_EVENTS = 0x0001
}
[Flags]
public enum CSMFilterType : byte
{
ENABLE_MSG = 0x00,
BLOCK_MSG = 0x01,
NO_FILTER = 0x02
}
[Flags]
internal enum STGC : int
{
DEFAULT = 0,
OVERWRITE = 1,
ONLYIFCURRENT = 2,
DANGEROUSLYCOMMITMERELYTODISKCACHE = 4,
CONSOLIDATE = 8
}
[Flags]
internal enum STGM : int
{
DIRECT = 0x00000000,
TRANSACTED = 0x00010000,
SIMPLE = 0x08000000,
READ = 0x00000000,
WRITE = 0x00000001,
READWRITE = 0x00000002,
SHARE_DENY_NONE = 0x00000040,
SHARE_DENY_READ = 0x00000030,
SHARE_DENY_WRITE= 0x00000020,
SHARE_EXCLUSIVE = 0x00000010,
PRIORITY = 0x00040000,
DELETEONRELEASE = 0x04000000,
NOSCRATCH = 0x00100000,
CREATE = 0x00001000,
CONVERT = 0x00020000,
FAILIFTHERE = 0x00000000,
NOSNAPSHOT = 0x00200000,
DIRECT_SWMR = 0x00400000
}

[ComImport]
[Guid("0000000b-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IStorage
{
void CreateStream(
/* [string][in] */ string pwcsName,
/* [in] */ uint grfMode,
/* [in] */ uint reserved1,
/* [in] */ uint reserved2,
/* [out] */ out ComTypes.IStream ppstm);
void OpenStream(
/* [string][in] */ string pwcsName,
/* [unique][in] */ IntPtr reserved1,
/* [in] */ uint grfMode,
/* [in] */ uint reserved2,
/* [out] */ out ComTypes.IStream ppstm);
void CreateStorage(
/* [string][in] */ string pwcsName,
/* [in] */ uint grfMode,
/* [in] */ uint reserved1,
/* [in] */ uint reserved2,
/* [out] */ out IStorage ppstg);
void OpenStorage(
/* [string][unique][in] */ string pwcsName,
/* [unique][in] */ IStorage pstgPriority,
/* [in] */ uint grfMode,
/* [unique][in] */ IntPtr snbExclude,
/* [in] */ uint reserved,
/* [out] */ out IStorage ppstg);
void CopyTo(
/* [in] */ uint ciidExclude,
/* [size_is][unique][in] */ Guid rgiidExclude, // should this be an array?
/* [unique][in] */ IntPtr snbExclude,
/* [unique][in] */ IStorage pstgDest);
void MoveElementTo(
/* [string][in] */ string pwcsName,
/* [unique][in] */ IStorage pstgDest,
/* [string][in] */ string pwcsNewName,
/* [in] */ uint grfFlags);
void Commit(
/* [in] */ uint grfCommitFlags);
void Revert();
void EnumElements(
/* [in] */ uint reserved1,
/* [size_is][unique][in] */ IntPtr reserved2,
/* [in] */ uint reserved3,
/* [out] */ out IEnumSTATSTG ppenum);
void DestroyElement(
/* [string][in] */ string pwcsName);
void RenameElement(
/* [string][in] */ string pwcsOldName,
/* [string][in] */ string pwcsNewName);
void SetElementTimes(
/* [string][unique][in] */ string pwcsName,
/* [unique][in] */ ComTypes.FILETIME pctime,
/* [unique][in] */ ComTypes.FILETIME patime,
/* [unique][in] */ ComTypes.FILETIME pmtime);
void SetClass(
/* [in] */ Guid clsid);
void SetStateBits(
/* [in] */ uint grfStateBits,
/* [in] */ uint grfMask);
void Stat(
/* [out] */ out ComTypes.STATSTG pstatstg,
/* [in] */ uint grfStatFlag);
}

[ComImport]
[Guid("0000000d-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IEnumSTATSTG
{
// The user needs to allocate an STATSTG array whose size is celt.
[PreserveSig]
uint
Next(
uint celt,
[MarshalAs(UnmanagedType.LPArray), Out]
ComTypes.STATSTG[] rgelt,
out uint pceltFetched
);
void Skip(uint celt);
void Reset();
[return:MarshalAs(UnmanagedType.Interface)]
IEnumSTATSTG Clone();
}
 
W

Walter Wang [MSFT]

Hi Ralf,

Based on my understanding, you are trying to pass an array of interface
pointers to a COM component, right?

To pass array in COM, you need SAFEARRAY.

Here's some key steps:

1) In your IDL:

[id(3), helpstring("method SendFiles2")] HRESULT SendFiles2([in,out]
SAFEARRAY(IUnknown*)* m);

2) Here's some example code on how to retrieve the element in a SAFEARRAY
with index 0:

STDMETHODIMP CSimpleObject::SendFiles2(SAFEARRAY ** m)
{
IUnknown* punk;
long elementNumber = 0;
HRESULT hr = S_OK;
hr = SafeArrayGetElement(*m, &elementNumber, &punk);
if (SUCCEEDED(hr)) {
IStream* pstream;
hr = punk->QueryInterface(IID_IStream, (void**)&pstream);
}
...

3) In C#, after you re-adding the reference, you should see this method now
has signature (ref Array). To call this method:

Array T = Array.CreateInstance(typeof(IStream), 1);
T.SetValue(stream, 0);
intf.SendFiles2(ref T);


Hope this helps.



Regards,
Walter Wang ([email protected], remove 'online.')
Microsoft Online Community Support

==================================================
When responding to posts, please "Reply to Group" via your newsreader so
that others may learn and benefit from your issue.
==================================================

This posting is provided "AS IS" with no warranties, and confers no rights.
 
R

Ralf

Hi Walter,

thanx for your answer.
But as I wrote in my question, I can't change anything at the interface of
the C++ COM component.
I need a way to use the existing interface without any modifications.

The interface is described in the IDL file as:

[
object,
uuid(A0926B7C-D57F-4F5E-9115-8F6ADB890DC1),
dual,
helpstring("IIntf Interface"),
pointer_default(unique)
]
interface IIntf : IDispatch
{
[id(21), helpstring("method SendFiles")]
HRESULT SendFiles(
[in] INT num,
[in, ref, size_is(num)] IUnknown** arr);

and my DevStudio generates:


using System;
using System.Runtime.InteropServices;

[ClassInterface(0)]
[TypeLibType(2)]
[Guid("A0926B7C-D57F-4F5E-9115-8F6ADB890DC1")]
public class CIntfClass :IIntf , CIntf
{
public CIntfClass ();
[DispId(1)]
public virtual void SendFiles(int num, ref object arr);
}



Regards
Ralf
 
W

Walter Wang [MSFT]

Hi Ralf,

I'm not sure if we could do that from .NET side but I will do some further
research.

Regards,
Walter Wang ([email protected], remove 'online.')
Microsoft Online Community Support

==================================================
When responding to posts, please "Reply to Group" via your newsreader so
that others may learn and benefit from your issue.
==================================================

This posting is provided "AS IS" with no warranties, and confers no rights.
 
W

Walter Wang [MSFT]

Hi Ralf,

Sorry for late reply.

I've done some research and the solution to this issue is to manually
define our interop assembly instead of using the one generated by Visual
Studio. The main cause is that the type library doesn't have the size_is()
attribute in the IDL. We must manually re-construct the interop assembly to
tell the interop marshaller that the first parameter of the method contains
the length of the array.

To modify the interop assembly, we can have several approaches:

1) Using ILDASM.exe to first disassemble the interop assembly generated by
tlbimp.exe or Visual Studio, then modify the IL code and later use
ILASM.exe to recompile the IL code into an assembly. This is described in
http://msdn2.microsoft.com/en-us/library/8zbc969t.aspx

2) Create the wrapper manually using the steps in MSDN document (How to:
Create Wrappers Manually,
http://msdn2.microsoft.com/en-us/library/x8fbsf00.aspx). With this approach
we can more easily to modify the familiar source code instead of learning
IL code.

3) Combining 1) and 2), we can use Reflector
(http://www.aisto.com/roeder/dotnet/) to first disassemble the interop
assembly into C# source code (using the FileGenerator plugin of Reflector
here: http://www.jasonbock.net/JB/CodeFileGenerator.aspx); then we can
modify it more easily.

I have done some test using approach 3), here's my modified C# source code
of the interop assembly:

public interface ISimpleObject
{
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType=MethodCodeType.Runtime), DispId(1)]
void SendFiles([In] int nstreams, [MarshalAs(UnmanagedType.LPArray,
ArraySubType=UnmanagedType.IUnknown , SizeParamIndex=0)] object[]
ppstreams);
...

public class SimpleObjectClass : ISimpleObject, SimpleObject
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType =
MethodCodeType.Runtime), DispId(1)]
public virtual extern void SendFiles([In] int nstreams,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.IUnknown,
SizeParamIndex = 0)] object[] ppstreams);
...


Note the key modifications we need to made is the MarshalAs attribute added
on the second parameter.

After made this changes, change your C# project to reference this interop
assembly and you can now easily use following code to call this method:

object[] arr2 = new object[] { stream , stream2};
intf.SendFiles(2, arr2);


Hope this helps.


Regards,
Walter Wang ([email protected], remove 'online.')
Microsoft Online Community Support

==================================================
When responding to posts, please "Reply to Group" via your newsreader so
that others may learn and benefit from your issue.
==================================================

This posting is provided "AS IS" with no warranties, and confers no rights.
 
W

Walter Wang [MSFT]

Hi Ralf,

I'm writing to check the status of this post. Please feel free to let me
know if there's anything else I can help. Thanks.


Regards,
Walter Wang ([email protected], remove 'online.')
Microsoft Online Community Support

==================================================
When responding to posts, please "Reply to Group" via your newsreader so
that others may learn and benefit from your issue.
==================================================

This posting is provided "AS IS" with no warranties, and confers no rights.
 
R

Ralf

"Walter Wang [MSFT]" said:
Hi Ralf,

I'm writing to check the status of this post. Please feel free to let me
know if there's anything else I can help. Thanks.


Regards,
Walter Wang ([email protected], remove 'online.')
Microsoft Online Community Support

==================================================
When responding to posts, please "Reply to Group" via your newsreader so
that others may learn and benefit from your issue.
==================================================

This posting is provided "AS IS" with no warranties, and confers no
rights.

Hi Walter,

thankx for your tips.
Inbetween I made also a lot of tests by my own and I found an easier
solution.

In thecode from my first message I found two bugs.

1. There was a pointer iteration to much.
2. The Marshall.GetIUnknownForObject call is dispensable.

With my new code it works with one restriction, I can only make a call with
one IStream reference.
This is no problem because I can put more than one file in a IStorage
object.

The only point I don't understand is, why I must not call
Marshall.GetIUnknownForObject.
Do you have an idea on it?
Does the interop assembly makes a GetIUnknownForObject call?

Regards
Ralf

public void SendFiles(string file)
{

FileStream fs = new FileStream(file, FileMode.Open);
StreamReader sr = new StreamReader(fs, Encoding.Default);
string trc = sr.ReadToEnd();
sr.Close();
fs.Close();

byte[] data = Encoding.Default.GetBytes(trc);

IStorage storage;
ComTypes.IStream stream;
StgCreateDocfile(null, STGM.CREATE | STGM.READWRITE | STGM.TRANSACTED |
STGM.SHARE_EXCLUSIVE | STGM.DELETEONRELEASE, 0, out storage);
storage.CreateStream("TRTFiles.trt", (uint)(STGM.READWRITE |
STGM.SHARE_EXCLUSIVE), 0,0, out stream);
stream.Write(data, data.Length, IntPtr.Zero);
stream.Commit((int)STGC.OVERWRITE);
storage.Commit((int)STGC.OVERWRITE);

object obj = stream;
this.intf.SendFiles(1, ref obj);
}
 
W

Walter Wang [MSFT]

Hi Ralf,

The Marshal.GetIUnknownForObject is used to return the interface pointer of
the COM Callable Wrapper (CCW,
http://msdn2.microsoft.com/en-us/library/f07c8z1c.aspx) for the object. A
COM Callable Wrapper is a proxy for the managed object for the COM clients.

In this case, if the COM component expects an IntPtr parameter to be used
as an interface pointer, then we can use GetIUnknownForObject() to pass the
CCW object's interface pointer around. However, the generated interop
assembly has signature "ref object". This instructs the default marshaller
to use designed marshalling behavior for object (IUnknown), since you're
passing an IntPtr Array, the marshaller has no idea what is exactly in the
IntPtr. Please see "Default Marshaling for Objects"
(http://msdn2.microsoft.com/en-us/library/2x07fbw8(VS.71).aspx) for more
information.


For your workaround, if you only have one object, then it works since
passing it by reference means the marshaller will pass the pointer
correctly. However, for more than one objects, we will have to use my
workaround in my last reply to tell the marshaller explicitly which
parameters contains the size of the array.


Regards,
Walter Wang ([email protected], remove 'online.')
Microsoft Online Community Support

==================================================
When responding to posts, please "Reply to Group" via your newsreader so
that others may learn and benefit from your issue.
==================================================

This posting is provided "AS IS" with no warranties, and confers no rights.
 

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