为啥 C++ 从 C# 中为返回自定义结构的函数获取错误的参数值

Posted

技术标签:

【中文标题】为啥 C++ 从 C# 中为返回自定义结构的函数获取错误的参数值【英文标题】:Why is C++ getting wrong parameter values from C# for a function returning a custom struct为什么 C++ 从 C# 中为返回自定义结构的函数获取错误的参数值 【发布时间】:2020-12-07 10:37:00 【问题描述】:

假设我有以下 C# 控制台应用程序调用 C++ 库。

internal static class Program

    private struct NumberContainer
    
        internal int Number1  get; 
        internal uint Number2  get; 

        internal NumberContainer(int number1, uint number2)
        
            Number1 = number1;
            Number2 = number2;
        

        public override string ToString()
            => $"Number1, Number2";
    

    [DllImport("CPlusPlusTargetLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
    private static extern int Add(int number1, uint number2);

    [DllImport("CPlusPlusTargetLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
    private static extern NumberContainer Compose(int signedInput, uint unsignedInput);

    private static void Main(string[] args)
    
        var unsignedInput = args.Any() && uint.TryParse(args.First(), out uint unsignedValue) ? unsignedValue : 0;
        var signedInput = args.Count() > 1 && int.TryParse(args.ElementAt(1), out int signedValue) ? signedValue : 0;

        Console.WriteLine("Adding 0 and 1...", signedInput, unsignedInput);
        Console.WriteLine("Result = 0", Add(signedInput, unsignedInput));

        Console.WriteLine("Composing 0 and 1...", signedInput, unsignedInput);
        Console.WriteLine("Composed 0", Compose(signedInput, unsignedInput));
    

以及CPlusPlusTargetLibrary.dll中的如下头文件:

#pragma once

struct NumberContainer

    const signed long Number1;
    const unsigned long Number2;

    NumberContainer(const signed long number1, const unsigned long number2)
        : Number1(number1),
        Number2(number2)
    
    
;

extern "C" __declspec(dllexport) signed long __cdecl Add
(
    signed long number1,
    unsigned long number2
);

extern "C" __declspec(dllexport) NumberContainer __cdecl Compose
(
    signed long signedInput,
    unsigned long unsignedInput
);

以及该标头的以下相应 CPP 文件:

#include "pch.h"
#include "TargetFunctions.h"

extern "C"
    
    __declspec(dllexport) signed long __cdecl Add(signed long number1, unsigned long number2)
    
        return number1 + number2;
    

    __declspec(dllexport) NumberContainer __cdecl Compose(signed long number1, unsigned long number2)
    
        return NumberContainer
        (
            number1,
            number2
        );
    

给定5作为C#中的unsignedInput-8作为signedInput,为什么Add正确接收-85return语句上的断点时它的 C++ 实现被击中,但 Compose 收到 5710724189?

另外,作为一个方面,一旦Number1(number1) 被命中,不可变结构NumberContainer 无法初始化抛出write access violation. 异常-尽管这是在收到错误值之后,所以这可能与我正在尝试解决的第一个问题。

如果我翻转调用顺序,Compose 然后会收到 51817521252,但由于上述异常,我无法继续查看 Add 收到的内容。

通过调试,我发现在任一实现的左大括号上放置断点都会显示不正确的参数值;在这一行之后,Add 的参数值都是正确的,但对于Compose,第一个参数始终是正确的,如果它是第二个参数,而第二个参数似乎是内存中的某个随机值。我在想一些东西抵消了论点,所以第一个在 C# 和 C++ 之间的某个地方被丢弃了,但是我不确定是怎么回事(我也尝试过将__stdcallCallingConvention.StdCallCallingConvention.Winapi 一起使用两者的最终结果相同)。

【问题讨论】:

如果你删除NumberContainer的构造函数,并让成员变为非const,会有什么变化吗? 您是否考虑过使用 cli/c++ 包装器?虽然它需要一个额外的项目,但它应该避免编组中的一些复杂性,因为您可以自己进行类型转换。 @canton7 是的,Compose 第一个 number1 现在以 5number2 作为 3146159005;翻转回来 Add 仍然正确输入,但 Compose410415680 出现 - 所以结构的不变性似乎不是问题的原因。 @JonasH 我没有,但听起来这可能是一个答案 ;-) 如果你拥有 c/c++ 代码,你应该把它变成一个 c++/CLI 项目。这使得与本机代码的对话变得微不足道,并且解决了麻烦的 p-invoke 【参考方案1】:

在这篇文章的帮助下我找到了答案:https://www.codeproject.com/Articles/66243/Marshaling-with-C-Chapter-3-Marshaling-Compound-Ty

我所要做的就是像这样用 StructLayoutAttribute 标记我的 C# NumberContainer 结构,然后参数值开始正确输入!

[StructLayout(LayoutKind.Sequential, Size = 64)]
private struct NumberContainer

    internal int Number1  get; 
    internal uint Number2  get; 

    internal NumberContainer(int number1, uint number2)
    
        Number1 = number1;
        Number2 = number2;
    

    public override string ToString()
        => $"Number1, Number2";

我相信之前发生的事情是结构被 C++ 假定为 32 位,因此 -8(一个 32 位有符号整数)的第一个参数构成了默认 64- 的第二半位 NumberContainer C++ 端的结构。但是,话虽如此:如果我将Size 更改为32128 并进行重建,它奇怪地继续传递正确的参数值!但是,如果属性中省略了Size,则参数值会像以前一样错误地输入。

我似乎已经解决了最初的问题,并且不可变结构成功实例化并从 C++ 代码返回,但上述关于Size 参数的谜团仍未解决。如果有人对此有答案,那么我很乐意接受它而不是我自己的答案。

【讨论】:

以下帖子可能会有所帮助:常见的 Visual C++ 64 位迁移问题:docs.microsoft.com/en-us/cpp/build/…,默认封送行为:docs.microsoft.com/en-us/dotnet/framework/interop/…,以及内置类型:docs.microsoft.com/en-us/dotnet/csharp/language-reference/…。

以上是关于为啥 C++ 从 C# 中为返回自定义结构的函数获取错误的参数值的主要内容,如果未能解决你的问题,请参考以下文章

为啥c++的set容器的自定义比较函数要用一个结构体重载()符

如何将结构数组从 c++ 传递到 c#?

为啥单击功能会在 Angular 2 中为自定义组件触发两次

将结构数组从 C# 传递到 C++

调用使用 C# 返回结构数组的非托管 dll 函数

如何从 C++ 后台任务(Windows 通用应用程序)调用 C# 函数?