为啥从针对任何 CPU 的 C# 项目调用此代码时会引发 System.AccessViolationException?

Posted

技术标签:

【中文标题】为啥从针对任何 CPU 的 C# 项目调用此代码时会引发 System.AccessViolationException?【英文标题】:Why does this code throw System.AccessViolationException when called from a C# project targeting Any CPU?为什么从针对任何 CPU 的 C# 项目调用此代码时会引发 System.AccessViolationException? 【发布时间】:2017-12-26 04:37:25 【问题描述】:

我的 ATL 项目中有这个 IDL:

[
    object,
    uuid(61B0BFF7-E9DF-4D7E-AFE6-49CC67245257),
    dual,
    nonextensible,
    pointer_default(unique)
]
interface ICrappyCOMService : IDispatch 
    typedef
        [
            uuid(C65F8DE6-EDEF-479C-BD3B-17EC3F9E4A3E),
            version(1.0)
        ]
    struct CrapStructure 
        INT ErrorCode;
        BSTR ErrorMessage;
     CrapStructure;
    [id(1)] HRESULT TestCrap([in] INT errorCode, [in] BSTR errorMessage, [in, out] CrapStructure *crapStructure);
;
[
    uuid(763B8CA0-16DD-48C8-BB31-3ECD9B9DE441),
    version(1.0),
]
library CrappyCOMLib

    importlib("stdole2.tlb");
    [
        uuid(F7375DA4-2C1E-400D-88F3-FF816BB21177)      
    ]
    coclass CrappyCOMService
    
        [default] interface ICrappyCOMService;
    ;
;

这是我在 C++ 中的实现:

STDMETHODIMP CCrappyCOMService::InterfaceSupportsErrorInfo(REFIID riid)

    static const IID* const arr[] = 
        &IID_ICrappyCOMService
    ;
    for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) 
        if (InlineIsEqualGUID(*arr[i], riid))
            return S_OK;
    
    return S_FALSE;


STDMETHODIMP CCrappyCOMService::TestCrap(INT errorCode, BSTR errorMessage, CrapStructure *crapStructure) 
    memset(crapStructure, 0, sizeof(CrapStructure));
    crapStructure->ErrorCode = errorCode;
    crapStructure->ErrorMessage = errorMessage;
    CComPtr<ICreateErrorInfo> x;
    ICreateErrorInfo* pCreateErrorInfo;
    CreateErrorInfo(&pCreateErrorInfo);
    pCreateErrorInfo->AddRef();
    pCreateErrorInfo->SetDescription(errorMessage);
    pCreateErrorInfo->SetGUID(IID_ICrappyCOMService);
    pCreateErrorInfo->SetSource(L"Component.TestCrap");
    IErrorInfo* pErrorInfo;
    pCreateErrorInfo->QueryInterface(IID_IErrorInfo, (void**)&pErrorInfo);
    pErrorInfo->AddRef();
    SetErrorInfo(0, pErrorInfo);
    pErrorInfo->Release();
    pCreateErrorInfo->Release();
    printf("Going to return %d...\n", errorCode);
    return errorCode;

我在 C# 中这样称呼它:

static void Main(string[] args)

    var service = new CrappyCOMService();
    var crapStructure = new CrapStructure();
    try
    
        service.TestCrap(-1, "This is bananas.", ref crapStructure);
    
    catch (COMException exception)
    
        Console.WriteLine(exception.ErrorCode);
        Console.WriteLine(exception.Message);
    
    Console.WriteLine(crapStructure.ErrorCode);
    Console.WriteLine(crapStructure.ErrorMessage);

如果我从针对 x64 的 C# 项目运行代码,那么一切正常。 问题是,当我从 C# 中针对 Any CPU 的项目中调用 TestCrap 方法时,它会抛出 System.AccessViolationException。这是为什么?我可以验证,当它抛出 System.AccessViolationException 时,它仍然会打印 Going to return -1... 到控制台窗口。

编辑:我缩小了复制步骤。抱歉,但我忘了添加这个。这似乎发生在使用 x64 构建(针对 x64 的 ATL 项目和针对任何 CPU 的 C# 测试项目)编译我的代码,然后转换回任何 CPU 构建(ATL 项目面向 Win32 和面向任何 CPU 的 C# 测试项目)。

编辑:我将错误范围缩小了一点。首先,看起来 Any CPU C# 项目总是使用我的 COM 对象的 x64 版本,因此我的 ATL 项目的版本需要首先编译才能传播任何代码,因为 x64 系统上的任何 CPU 真的会在 x64 上下文中运行。此外,看起来crapStructure-&gt;ErrorMessage = errorMessage; 导致了System.AccessViolationException。它将在 COM 世界中执行代码,但在返回 C# 世界时会抛出异常。

编辑:在 C++ 中,printf("%d\n", sizeof(CrapStructure)); 结果为 16。在 C# 中,Console.WriteLine(Marshal.SizeOf(typeof(CrapStructure))); 也会产生 16。但是,唉,多读一些,这是一个无用的检查,正如汉斯在这里的回答中提到的那样:How do I check the number of bytes consumed by a structure?

编辑:我尝试在我的Any CPU-targeted C# 项目中使用tlbimp CrappyCOM.dll /out:CrappyCOMx86Managed.dllCrappyCOMx86Managed.dll,但它再次抛出了System.AccessViolationException。我想这是因为它再次转到系统上注册的 x64 COM 对象,所以我使用regsvr32 取消注册 x64 COM 对象。再次运行代码并注册了 x86 COM 对象,它抱怨 检索具有 CLSID F7375DA4-2C1E-400D-88F3-FF816BB21177 的组件的 COM 类工厂失败,原因是以下错误:80040154 类未注册(来自 HRESULT 的异常:0x80040154 (REGDB_E_CLASSNOTREG))。 我发现使它从我的tlbimp 生成的存根中以 x86 COM 对象为目标的唯一方法是将我的 C# 项目的构建目标修改为 x86,并且然后代码可用于测试我的 x86 COM 对象,所以这对我来说似乎是一个有争议的问题,因为我希望任何 CPU 专门针对我的 x86 COM 对象或具有正确结构大小的 x64 COM 对象。 COM 在 C# 中是一场灾难。

【问题讨论】:

您可以尝试通过日志和指针检查在c++ 代码中缩小范围,以查看究竟是哪个调用导致了异常。 尝试在没有ref 的情况下传递crapStructure。喜欢这个service.TestCrap(-1, "This is bananas.", crapStructure); @Griffin 我更新了我的问题,因为我把它缩小了一点。此外,不能使用ref 作为它自动生成的代理存根类,因为它在IDL 中标记为[in, out] C#代码中为This is bananas.分配内存并传递变量。 @Griffin 如何从 C# 世界中为该字符串分配内存? 【参考方案1】:

可能发生的情况是您(隐含地)在 x64 和 x86 版本中使用相同的类型库 (.TLB) 或包含 .TLB 的 DLL。

问题是您的 TLB 定义了一个非托管结构,并且此结构布局在 32 位和 64 位模式下会有所不同(大小、偏移量等)。

从 .NET 中,您希望确保在使用不同的处理器架构进行编译时引用的不是完全相同的互操作程序集(通过隐式 tlbimp COM 引用从 TLB 生成)。

使用标准的 Visual Studio 工具来正确处理它可能会很棘手。

如果您仍想使用这样的结构(这在自动化世界中有点奇怪),我建议您自己使用 tlbimp.exe 构建互操作程序集,并像普通 .NET 程序集一样简单地引用这些互操作程序集,但取决于关于位数(您将能够使用 .csproj 中的“条件”属性进行调整),而不是直接使用 COM 引用。

【讨论】:

嘿西蒙,我已经编辑了我的问题,因为我把它缩小了一点。它似乎在调用我的 COM 对象的 x64 版本。问题是crapStructure-&gt;ErrorMessage = errorMessage; 导致了该异常,但我不知道为什么 - 不过,我想知道您的建议是否有助于解决这个问题。你有什么想法?如果它调用我的 COM 方法的 x64 版本,我是否应该期望它为其结构设置正确的大小,或者不是这里的情况,因为它的目标是任何 CPU? 是的,这是 x86 与 x64 中 CrapStructure 布局的问题。如果您使用 C# x64,则必须使用基于 x64 版本的 TLB 的互操作程序集,因此 CrapStructure 针对 x64 进行了正确描述。 哦,我明白你现在在说什么了。您是说我应该测试使用tlbimp.exe 在 C# 中生成代理存根的两个版本后会发生什么。伙计,我不能感谢你。一次又一次,在 *** 上,你帮了我很多!我希望我能和你这样的人一起工作,向你学习更多。 嘿西蒙,我刚刚编辑了这个问题。如果我的 C# 项目设置为针对任何 CPU,您是否知道是否有任何方法可以针对我的 COM 对象的 x86 版本?尽管我尝试使用 tlbimp.exe 来使用 x86 COM 对象的输出 DLL,但它似乎一直想要使用 x64 实现。 就像我说的,工具很棘手 :-) 你选择了这种结构的危险路径,因为这意味着 TLB 是位敏感的,这有点不寻常。最简单的做法是停止这样做并保留纯自动化类型。否则,请确保删除“COM 引用”,并在 interop/per-bitness/tlbimp 生成的 .net 程序集上跟上标准 .NET 引用,就像您在上次编辑时所做的那样。 Marshal.Sizeof(CrapStructure) 在 x86 中为 8,在 x64 中为 16。在 C# 中使用 IntPtr.Size 来确定位数。取消选中 C# 项目道具中的“首选 32 位”,对于您的测试,不要使用 AnyCpu。

以上是关于为啥从针对任何 CPU 的 C# 项目调用此代码时会引发 System.AccessViolationException?的主要内容,如果未能解决你的问题,请参考以下文章

C# 编译为 32/64 位,或任何 cpu? [复制]

从 C# 调用 F# 代码

为啥不能在 C# 中删除未使用的引用

为啥从内部类调用特定于对象的方法/字段时没有抛出任何错误?

C#编译过程

从 C# 调用 C++ 代码而不创建 dll?