COM互操作:如何使用ICustomMarshaler调用第三方组件

4

我希望使用COM互操作从C#调用COM组件中的方法。以下是该方法的签名:

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

以下是示例代码(我已经检查过,确实可用)在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);
}

在导入COM组件的类型库后,这是相应的C#签名:

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

如果我没记错的话,我需要传入null值给pvarPrecursorInfos参数,这样COM互操作才能将其作为预期的VT_EMPTY变体进行编组。但当我这样做时,我会遇到SafeArrayTypeMismatchException异常——这并不令人惊讶,因为在示例中结果的处理方式已经很明显了。因此,我尝试使用自定义编组器。由于无法更改组件本身,所以我试图通过以下方式引入它:

[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);
}

TypeLibType和DispID属性与原始版本相同。在调用MyMarshaller.GetInstance()方法时,这个方法能够正常运行,但是我没有在MyMarshaller.NativeToManaged中得到回调。相反,会报告一种访问冲突。那么这是一个可靠的方法吗?如果是,如何使其工作?如果不是,有其他的替代方案吗?
(顺便提一句:理论上,我可以尝试使用托管C++来本地调用组件。然而,在它里面还有很多其他的方法可以很好地与COM互操作,因此如果有任何办法,我非常希望继续使用C#。)

1
这里有任何进展吗?我正在尝试从C#访问完全相同的方法。 - Gordon Slysz
嗨,马蒂亚斯,我仍然对这个问题的纯C#解决方案或“托管C++”解决方案感兴趣。 - Dmitry Avtonomov
@Matthias,希望您能注意到这个求助请求 :) - Dmitry Avtonomov
@chhh 当然了,但是有两个小孩和一份工作,需要花些时间来找到解决方案。希望能有所帮助。 - Matthias
感谢@Vlad添加了C++标签。我不知道这会影响代码块的呈现。 - Matthias
显示剩余5条评论
2个回答

2

有人问起,这是我的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;
}

谢谢!有点奇怪,有人已经给答案打了负分,所以我的+1并没有什么帮助。这应该放在一个单独的C++ CLR项目中吗? - Dmitry Avtonomov
1
@chhh 嗯,就声望积分而言,我仍然是+8 - 谢谢。但无论如何:正是因为有这样的不愉快经历,我通常都避免在SO上积极参与。- 是的,你需要把它放到一个单独的项目中。我有一个C#门面,用于我实际使用的方法,该门面根据方法直接委托给COM组件或C++代理。如果您想遵循这种方法,请确保将打开、关闭等内容分派给两个通道。 - Matthias
我有一个后续问题。我创建了一个托管的C++/CLI类,该类创建了一个非托管的COM接口实例。我将指向该接口的指针(非托管的*)保存为类的成员,在构造函数中的代码中,我可以执行所有库操作(打开、设置当前控制器...),但在构造函数返回后,任何使用COM指针的后续调用都会失败,并出现一些AccessViolationException异常。您能否分享更多关于此托管/非托管互操作性的实现细节?只需要C++类的基本框架即可。 - Dmitry Avtonomov
我终于解决了这个问题,但是我不明白一开始的问题是什么。我不得不为XRawfile2.dll创建一个完全未管理的c++包装器,然后再为未管理的包装器创建另一个托管包装器,将此未管理的包装器存储为字段... 嗯... 至少在这种情况下,我的指针没有疯狂/空指针,并且可以用于获取像前体扫描号这样简单的东西。再次感谢Matthias! - Dmitry Avtonomov

0

我查看了Github上的mzLib代码,我相信它与这个主题有关。代码看起来很不错,在调用时表现良好。

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

我认为这可能会引起一些问题,最好使用

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

代码在编译时似乎很好地运行。但是,当作为dll导入时可能会遇到问题。我通过添加构造函数来解决这个问题,例如:
public ref class ThermoDLLClass 
{
public:
    ThermoDLLClass();
    PrecursorInfo GetPrecursorInfo(int scanNum, String^ path);


};

然后,它似乎适当地获取了 precursorInfo 和参数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接