Matthias
Matthias

Reputation: 1799

COM interop: how to use ICustomMarshaler to call 3rd party component

I want to call a method in a COM component from C# using COM interop. This is the methods signature:

long GetPrecursorInfoFromScanNum(long nScanNumber,
LPVARIANT pvarPrecursorInfos,
LPLONG pnArraySize)

and this is sample code (which I checked is really working) to call it in C++:

struct PrecursorInfo
{
    double dIsolationMass;
    double dMonoIsoMass;
    long nChargeState;
    long nScanNumber;
};

void CTestOCXDlg::OnOpenParentScansOcx()
{
    VARIANT vPrecursorInfos;
    VariantInit(&vPrecursorInfos);
    long nPrecursorInfos = 0;

    m_Rawfile.GetPrecursorInfoFromScanNum(m_nScanNumber,
        &vPrecursorInfos,
        &nPrecursorInfos);

    // Access the safearray buffer
    BYTE* pData;
    SafeArrayAccessData(vPrecursorInfos.parray, (void**)&pData);
    for (int i=0; i < nPrecursorInfos; ++i)
    {
        // Copy the scan information from the safearray buffer
        PrecursorInfo info;
        memcpy(&info,
        pData + i * sizeof(MS_PrecursorInfo),
        sizeof(PrecursorInfo));
    }
    SafeArrayUnaccessData(vPrecursorInfos.parray);
}

And here's the corresponding C# signature after importing the typelib of the COM component:

void GetPrecursorInfoFromScanNum(int nScanNumber, ref object pvarPrecursorInfos, ref int pnArraySize);

If I'm not mistaken, I need to pass in null for pvarPrecursorInfos to make COM interop marshal it as the expected VT_EMPTY variant. When I'm doing it, I get a SafeArrayTypeMismatchException - not really surprising, looking at how the result is expected to be handled in the sample. So I was trying to use a custom marshaler. Since a cannot alter the component itself, I tried to introduce it this way:

[Guid("06F53853-E43C-4F30-9E5F-D1B3668F0C3C")]
[TypeLibType(4160)]
[ComImport]
public interface IInterfaceNew : IInterfaceOrig 
{
    [DispId(130)]
    int GetPrecursorInfoFromScanNum(int nScanNumber, [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(MyMarshaler))] ref object pvarPrecursorInfos, ref int pnArraySize);
}

The TypeLibType and DispID attribute are the same as in the original version. This works as far as that the MyMarshaller.GetInstance() method is called, but I do not get a callback in MyMarshaller.NativeToManaged. Instead, an access violation is reported. So is this a reliable approach? If yes - how can I make it work? If no: are there any alternatives?

(Just a footnote: in theory I could try to use managed C++ to call the component natively. However, there are lots of other methods in it that work fine with COM interop, so I would very much like to stick with C# if there is any way.)

Upvotes: 4

Views: 663

Answers (2)

Rui Zhang
Rui Zhang

Reputation: 1

I look at the github code mzLib, which I believe is related to this topic. The code looks good, where it calls

pin_ptr<const wchar_t> wch = PtrToStringChars(path);

I think it may cause some problem, better use

pin_ptr<const wchar_t> pathChar = static_cast<wchar_t*>(System::Runtime::InteropServices::Marshal::StringToHGlobalUni(path).ToPointer());

The code then seems to be worked just fine when compiles. However, it might run in problem when imported as dll. I worked on that by adding a constructor,such as

public ref class ThermoDLLClass 
{
public:
    ThermoDLLClass();
    PrecursorInfo GetPrecursorInfo(int scanNum, String^ path);


};

Then, it seems to get precursorInfo and parameters appropriately.

Upvotes: 0

Matthias
Matthias

Reputation: 1799

Since someone asked for it, here's my solution in Managed C++.

array<PrecursorInfo^>^ MSFileReaderExt::GetPrecursorInfo(int scanNumber)
{
    VARIANT vPrecursorInfos;
    VariantInit(&vPrecursorInfos);
    long nPrecursorInfos = -1;

    //call the COM component
    long rc = pRawFile->GetPrecursorInfoFromScanNum(scanNumber, &vPrecursorInfos, &nPrecursorInfos);

    //read the result
    //vPrecursorInfos.parray points to a byte sequence
    //that can be seen as array of MS_PrecursorInfo instances
    //(MS_PrecursorInfo is a struct defined within the COM component)
    MS_PrecursorInfo* pPrecursors;
    SafeArrayAccessData(vPrecursorInfos.parray, (void**)&pPrecursors);

    //now transform into a .NET object 
    array<PrecursorInfo^>^ infos = gcnew array<PrecursorInfo^>(nPrecursorInfos);

    MS_PrecursorInfo currentPrecursor;
    for (int i=0; i < nPrecursorInfos; ++i)
    {
        currentPrecursor = pPrecursors[i];

        infos[i] = safe_cast<PrecursorInfo^>(Marshal::PtrToStructure(IntPtr(&currentPrecursor), PrecursorInfo::typeid));
    }

    SafeArrayUnaccessData(vPrecursorInfos.parray);

    return infos;
}

Upvotes: 2

Related Questions