none
C# interop with unmanaged C++ ActiveX issue - customizing wrapper to pass float array to COM object RRS feed

  • Question

  • Thanks for reading my post. 

    <Edit: I made 'some' progress, see Reply below, but the problem remains. Read this to understand the context.>

    I need to start with a warning: I don't think this problem is simple; I have been working on it for over 3 weeks now, and I am still stuck, so please refrain from giving a 2 sentence advice unless you think you know of a solution or have some useful insight on the whole process, which I certainly need to understand better.


    I posted on the 2 following forums, to explain the process:
    http://social.msdn.microsoft.com/Forums/en-US/csharpgeneral/thread/f5de0a60-0cd9-43d9-b27e-1d2a7de248d5/
    http://social.msdn.microsoft.com/Forums/en-US/clr/thread/dd776c69-d67b-4541-8a26-3aeb37f2fd96/

    but I think I am still missing some understanding of the way Visual C# handles standard interops.

    Where I start:
    I need to use an ActiveX (ocx) control, from a software package. This ActiveX (let's call it AX) allows me to open and read data from the package's proprietary file format. This ActiveX is not a stand alone: it uses/links to other DLLs too. It is registered in the directory where it calls the other files.

    When I load the ActiveX in the VS C# toolbox, C# creates 2 files AxInterop.AXLib.dll and Interop.AXLib.dll as well as references to those 2 DLLs in the project.

    When I drop the control on a form, the code to initialize it etc... is generated in the designer.
    All seems to work here as long as I use functions with parameters that are marshalled to standard Types.

    Trouble starts when I want to use a function to send data back into a file to then save it in the proprietary format.
    The data is stored in an array of floats (float[]), and in PE explorer the function is defined as

    function PutValueEx(out ID: I4; out pName: BSTR; out pFloatValue: ^R4; out Size: I4): I4; stdcall;

    That is, the array should be sent as a pointer to a float (^R4, or float* in C++) with the size defined by an Int32
    However, in converting in C#, the interop library defines it as:

    public virtual int PutValueEx(int iD, string pName, ref float pFloatValue, int size)

    So the ' float* ' becomes a ' ref float '

    So here is a first question: how is a 'ref' type passed on to a COM object? Is it passed as a pointer to the value? if this is the case, then it should work, but passing the reference to the first object in my array does not work, and give me an 'TYPE_MISMATCH' error.

    So I went into editing the interop assembly as explained in my posts:
    http://social.msdn.microsoft.com/Forums/en-US/csharpgeneral/thread/f5de0a60-0cd9-43d9-b27e-1d2a7de248d5/
    http://social.msdn.microsoft.com/Forums/en-US/clr/thread/dd776c69-d67b-4541-8a26-3aeb37f2fd96/

    and tried so many things I am not sure where I am now.

    So, I have a second question regarding this process:
    When I use AxImp.exe to import the ActiveX and generate the 2 DLL (this time named AxAXLib.dll and AXLib.dll), the 'AxAXLib.dll' generated is smaller than the 'AxInterop.AXLib.dll' generated when importing the ocx in the toolbox.
    Looking at the Interface Definition, there is a lot more stuff in the AxInterop.AXLib.dll assembly generated when importing through the toolbox than the one generated by AxImp.exe; the missing stuff seems related to the definition of the functions to make the ActiveX a Windows.Form object.
    So the question is: Am I missing something in using AxImp.exe that the generated DLL is much smaller?

    A 3rd question is: how does the managed wrapper connects to the unmanaged COM object? Does it end up calling the original ocx, and locates it through the registry entry? My ocx still needs to be able to communicate with the other DLLs of the software package and I am not sure I am keeping this link when editing the MSIL.

    I am also confused as for why there are 2 DLLs generated by AxImp or the toolbox import: one must be the interop assembly, but what is the role of the other one? I thought it was a 'transformed' version of the original ocx, but the original ocx must still be used if things are to work with the other DLL in the package. It seems like there is an 'ActiveX wrapper' and then a 'COM wrapper'.

    Another odd things I noticed is the following:
    After getting the 2 DLLs with AxImp.exe, and obtaining the C# source code for the 1st assembly (AxAXLib.cs = source for AxAXLib.dll), I edit the MSIL of the second DLL: I disassemble the DLL with Ildasm.exe, edit the MSIL, then reassemble with Ilasm.exe
    The resulting DLL is again smaller than the original one, and this time again I am not sure what is being lost...

    My last question is going to be: is there a way to trace/debug a call to an unmanaged COM object like this one beyond the call from the interop assembly?
    My problem is that I have no idea where things are wrong: i only get either a TYPE_MISMATCH error, a MEMBERNOTFOUND error, or a 'Attempt to read or write protected memory' when calling that function that's causing me trouble, while other calls work fine. I cannot tell if it's an issue of the parameter type not being carried over properly to the COM object, if it's a problem with rgistration, if i forgot something somewhere, or anything...

    Finally, a comment: when rebuilding my project, VS seems to always overwrite the modified DLLs, unless I unreference the old ones and reference the ones I created... I still wonder how those find the original OCX to work with.

    Answers or explanation on these questions would be helpful...
    Thanks for help.





    I forget to mention I also got the function parameter changed to replace the FLOAT* to a LPUNKNOWN*

    As a result, the interop library function became

    PutValueEx(int ID, string pName, object pFloatValue, int size);

    So now I can pass anything (an object) to the function, and it's waiting for a pointer on the array (LPUNKNOWN*), the size of the array, and casting to float (in the function).

    The code is as follow: (with axActiveX1 being the control on the form)

                    //Load a file
                    int Id = axActiveX1.Load(openFileDialog1.FileName);

                    //Get Data from the file, by passing a null object to the GetValue function.
                    object dataobj = null;
                    axActiveX1.GetValue(Id, "Data", ref dataobj);

                    //The Data is now in 'dataobj' and needs to be copied into an array of float for further processing
                    Array data = (Array)dataobj;
                    float[] d = new float[data.Length];
                    for (int i = 0; i < data.Length; i++)
                    {
                        d[i] = (float)data.GetValue(i);
                    }
                    //Some processing done here...
                  
                    //Now reverse process, copy the processed data into a marshalled COM buffer
                    IntPtr buffer = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(float)) * d.Length);
                    Marshal.Copy(d,0,buffer,d.Length);

                    //And now the problematic function:
                    axActiveX1.PutValueEx(Id, "Data", buffer, d.Length);

    I tried passing buffer.ToInt32(), 
    I tried passing only 1 value of the array,
    I tried using Marshal.AllocCoTaskMem instead of Marshal.AllocHGlobal,
    I tried passing the pointer to the array locked with GCHandle.Alloc(d, GCHandleType.Pinned)
    I tried passing 'dataobj' directly back as the 'object'

    I always get an error:

    'Attempt to read or write protected memory


    That's where I'm starting to wonder if there is an issue on the ActiveX side, or something funny in the way the data is passed...

    Once again, any suggestion would be greatly appreciated.

    Thanks for help!


    • Edited by WiizzZZZ Tuesday, June 16, 2009 6:16 AM
    Sunday, June 14, 2009 7:36 PM

Answers

  • Sorry to hear you're still having problems with this. Normally it shouldn't be this hard.

    Now to your questions:

    >how is a 'ref' type passed on to a COM object? Is it passed as a pointer to the value?

    It's indeed passed as a pointer to a value, but it can be a copy of the value on the managed side. So you can't use a ref parameter and pass in the first element of an array and expect it to work if the callee expects an entire array. That's why you need to change the parameter type to a proper array type.

    >So the question is: Am I missing something in using AxImp.exe that the generated DLL is much smaller?

    Good question, I'd be interested in seeing why as well. There are some differences in what VS and AxImp does, but usually they are not significant. VS has a different naming convention for the assembly names and namespace, and VS generates the interop assembly as if the /sysarray argument was passed to Tlbimp. But other than that, the result should be pretty much the same. Can you share the assemblies with us so we can have a closer look?


    > A 3rd question is : how does the managed wrapper connects to the unmanaged COM object? Does it end up calling the original ocx, and locates it through the registry entry?

    It uses the standard COM APIs. Ie creates the object with CoCreateInstance, which in turn looks in the Registry or in an xml manifest file to locate the OCX.


    > My last question is going to be: is there a way to trace/debug a call to an unmanaged COM object like this one beyond the call from the interop assembly?

    If you have the source code and debug symbols for the COM component, and enable unmanaged code debugging in your VS project, you can set a breakpoint in the native code.


    A couple of other things. Changing the parameter type to LPUNKNOWN and then casting to a float* in native code is not the way to go. Those are two completely unrelated types. But since you apparently can change the COM interfaces, why don't you follow nobugz suggestion from one of your other threads and use SAFEARRAY(float) as the parameter type? That would make the import tools give you the correct type right away. Since you get TYPE_MISMATCH and MEMBERNOTFOUND errors, it sounds like the call may go through IDispatch, in which case it's critical that your parameter types match.


    Mattias, C# MVP
    • Marked as answer by WiizzZZZ Tuesday, June 16, 2009 6:41 PM
    Tuesday, June 16, 2009 12:34 PM
    Moderator



  • Report after trying something else:

    Dezorian here: http://social.msdn.microsoft.com/Forums/en-US/csharpgeneral/thread/f5de0a60-0cd9-43d9-b27e-1d2a7de248d5/?prof=required

    suggested that I try using VariantWrapper.

    The original function before I had it modified by the vendor was taking a Variant*

    I tried, and it works... well, kinda: it goes through, and I get my data to the ActiveX without error.
    But the data is cropped (I have to say I can't tell if it is the beginning or the end though...)

    So I'm looking into this issue now... but there is progress!
    • Marked as answer by WiizzZZZ Wednesday, June 17, 2009 3:11 PM
    Wednesday, June 17, 2009 3:11 PM

All replies

  • Sorry to hear you're still having problems with this. Normally it shouldn't be this hard.

    Now to your questions:

    >how is a 'ref' type passed on to a COM object? Is it passed as a pointer to the value?

    It's indeed passed as a pointer to a value, but it can be a copy of the value on the managed side. So you can't use a ref parameter and pass in the first element of an array and expect it to work if the callee expects an entire array. That's why you need to change the parameter type to a proper array type.

    >So the question is: Am I missing something in using AxImp.exe that the generated DLL is much smaller?

    Good question, I'd be interested in seeing why as well. There are some differences in what VS and AxImp does, but usually they are not significant. VS has a different naming convention for the assembly names and namespace, and VS generates the interop assembly as if the /sysarray argument was passed to Tlbimp. But other than that, the result should be pretty much the same. Can you share the assemblies with us so we can have a closer look?


    > A 3rd question is : how does the managed wrapper connects to the unmanaged COM object? Does it end up calling the original ocx, and locates it through the registry entry?

    It uses the standard COM APIs. Ie creates the object with CoCreateInstance, which in turn looks in the Registry or in an xml manifest file to locate the OCX.


    > My last question is going to be: is there a way to trace/debug a call to an unmanaged COM object like this one beyond the call from the interop assembly?

    If you have the source code and debug symbols for the COM component, and enable unmanaged code debugging in your VS project, you can set a breakpoint in the native code.


    A couple of other things. Changing the parameter type to LPUNKNOWN and then casting to a float* in native code is not the way to go. Those are two completely unrelated types. But since you apparently can change the COM interfaces, why don't you follow nobugz suggestion from one of your other threads and use SAFEARRAY(float) as the parameter type? That would make the import tools give you the correct type right away. Since you get TYPE_MISMATCH and MEMBERNOTFOUND errors, it sounds like the call may go through IDispatch, in which case it's critical that your parameter types match.


    Mattias, C# MVP
    • Marked as answer by WiizzZZZ Tuesday, June 16, 2009 6:41 PM
    Tuesday, June 16, 2009 12:34 PM
    Moderator
  • Hi Mattias,

    Thanks for clarification. That helps understanding the process.

    Unfortunately I do not have the original code, and won't get it. Plus it would be in C++ and I'm working in C#. I'm not sure how I'd go about mixing the 2 and debugging...

    I did get the vendor to change the parameter in the function, and did ask to change to SafeArray, but they said it would require changing a lot more code and the 'easy' solution was to just change the type to LPUNKNOWN* (the type they thought was most appropriate since void* is apparently not available in ActiveX)

    Yes I know SafeArray would be the best way... I'll try again to ask, but this may take time.

    Further question: Is there a Type that would be more related to float* or void* that would be more appropriate?

    I can share the assemblies if you'd like; how do I do that, and what do you need? Note again that the ocx is part of a software package, and it likely accesses other DLLs in that package, so to truely debug it would require that you install that software. I'd be glad to provide a copy if you can help...
    but I guess that to look at the assembly you just need the ocx.

    Thanks for help.

    Regards
    Tuesday, June 16, 2009 4:25 PM
  • Hmya, this just isn't getting any better, is it?  LPUNKNOWN is an interface pointer (IUnknown*), truly unusable since arrays are not classes with interfaces.  Making it LPUNKNOWN* is a sure sign that you are working with a programmer that just doesn't know what s/he's doing.  There are 3 possible ways I see to get out of this mess:

    - Tighten the spec, insist that the vendor's component is usable from a scripting language like any ActiveX component.  That forces them to implement it properly.
    - Learn to program in C++ yourself so you can write a wrapper that can give the vendor the raw pointer they need.
    - Hand this problem off to management.

    You've struggled with this long enough to warrant option #3.  Talk to your supervisor.

    Hans Passant.
    Tuesday, June 16, 2009 5:20 PM
    Moderator
  • Hi Nobugz

    Yes this is a mess... and I am pretty sure this feature was never tested.
    This ActiveX is rarely used as far as I understand, at least not in the way I use it...
    ...and I trust the programmers know win32 MFC programming pretty well, but have little experience with CLR and .NET

    Politically, it's going to be difficult to push... let me handle that part of the problem.
    What I need to know is what is the best way to do this, and what I need to get the programmer to do for me.

    The consensus seems to be that I need them to use a SafeArray

    Is this your recommendation?
    Any more information you could give to make sure they get this and the spec is clear?

    Let me know!

    Thanks for help.

    Regards
    Tuesday, June 16, 2009 6:05 PM
  • Your trust is misplaced, this programmer truly doesn't know COM programming.  SAFEARRAY is the only suitable type to make this a usable ActiveX component.  Once you got that, using it from C# is trivial.  Pursue the "has to be usable from a scripting language" angle.
    Hans Passant.
    • Marked as answer by WiizzZZZ Tuesday, June 16, 2009 6:41 PM
    • Unmarked as answer by WiizzZZZ Wednesday, June 17, 2009 3:15 PM
    Tuesday, June 16, 2009 6:24 PM
    Moderator
  • i guess this is going to be the final answer...

    I'll try to get it changed and let you know!

    Thanks again for the help!
    Tuesday, June 16, 2009 6:41 PM

  • - Learn to program in C++ yourself so you can write a wrapper that can give the vendor the raw pointer they need.

    Hans Passant.

    Hi Nobugz...

    Actually, if no amount of political pressure works... I guess i'll have to go with your option #2: learn C++ and write a wrapper.

    Would I then need to make a wrapper for the ActiveX, or could it be possible to make a wrapper for that specific function only? I imagine I'd need to send my array to the wrapper, which should then know to use the same ActiveX instance I use in the C# program, and call the function from the wrapper passing the raw pointer to it...

    Do you think that'd be a viable option?
    Tuesday, June 16, 2009 7:09 PM



  • Report after trying something else:

    Dezorian here: http://social.msdn.microsoft.com/Forums/en-US/csharpgeneral/thread/f5de0a60-0cd9-43d9-b27e-1d2a7de248d5/?prof=required

    suggested that I try using VariantWrapper.

    The original function before I had it modified by the vendor was taking a Variant*

    I tried, and it works... well, kinda: it goes through, and I get my data to the ActiveX without error.
    But the data is cropped (I have to say I can't tell if it is the beginning or the end though...)

    So I'm looking into this issue now... but there is progress!
    • Marked as answer by WiizzZZZ Wednesday, June 17, 2009 3:11 PM
    Wednesday, June 17, 2009 3:11 PM