.NET Forum / Languages / C# / January 2008
accessing COM from C# with an IStream** parameter
|
|
Thread rating:  |
Ralf - 16 Jan 2008 19:14 GMT 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(); }
Walter Wang [MSFT] - 17 Jan 2008 07:34 GMT 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 (wawang@online.microsoft.com, 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.
Ralf - 17 Jan 2008 19:45 GMT 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
> Hi Ralf, > [quoted text clipped - 46 lines] > This posting is provided "AS IS" with no warranties, and confers no > rights. Walter Wang [MSFT] - 18 Jan 2008 09:39 GMT Hi Ralf,
I'm not sure if we could do that from .NET side but I will do some further research.
Regards, Walter Wang (wawang@online.microsoft.com, 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.
Walter Wang [MSFT] - 22 Jan 2008 09:42 GMT 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 (wawang@online.microsoft.com, 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.
Walter Wang [MSFT] - 25 Jan 2008 02:55 GMT 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 (wawang@online.microsoft.com, 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.
Ralf - 27 Jan 2008 20:47 GMT > Hi Ralf, > [quoted text clipped - 12 lines] > 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); }
Walter Wang [MSFT] - 28 Jan 2008 05:59 GMT 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 (wawang@online.microsoft.com, 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.
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|