在C#中调用PInvoke DLL

5

我希望将一个结构体传递给C函数,我编写了以下代码。

当我运行它时,第一个函数Foo1可以正常工作,但是函数Foo会收到异常。你能帮我理解问题出在哪里吗?...

C代码:

typedef struct 
{
    int Size; 
    //char *Array;
}TTest;

__declspec(dllexport) void Foo(void  *Test);
__declspec(dllexport) int  Foo1();

void Foo(void  *Test)
{
    TTest *X = (TTest *)Test;
    int i = X->Size;
    /*for(int i=0;i<Test->Size;Test++)
    {
        Test->Array[i] = 127;
    }*/
}

int Foo1()
{
    return 10;
}

C#代码:

using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    [StructLayout(LayoutKind.Sequential)]
    public class TTest 
    {
        public int Size; 
    }

    class Program
    {
        [DllImport(@"C:\.net course\unmanaged1\unmanaged3\Debug\unmanaged3.dll", CharSet = CharSet.Auto)]
        public static extern void Foo(
            [MarshalAs(UnmanagedType.LPStruct)]
            TTest lplf   // characteristics
        );

        [DllImport(@"C:\.net course\unmanaged1\unmanaged3\Debug\unmanaged3.dll", CharSet = CharSet.Auto)]
        public static extern int Foo1();

        static void Main(string[] args)
        {
            TTest Test = new TTest();
            Test.Size = 25;

            int XX = Program.Foo1();
            Program.Foo(Test);
        }
    }
}

2
去掉[MarshalAs(UnmanagedType.LPStruct)],并在你的两个DllImport中添加CallingConvention = CallingConvention.Cdecl,这样就可以了。 - ildjarn
请在您的问题中添加异常详细信息和其他可能有助于回答问题的信息。 - CharlesB
2个回答

6
对于那些给答案投反对票的人:这个答案解决了两个问题:调用约定/ MarhsalAs 属性的立即问题,以及他将很快发现的问题,即如果他采用我的建议将 TTest 转换为结构体,则他的 TTest 参数将无法正常工作。
你的本机代码正在请求一个 void*,在 C# 中是一个 IntPtr。首先,你应该将 TTest 定义为结构体而不是类。其次,你应该更改 Foo 的声明为:
[DllImport(@"C:\.net course\unmanaged1\unmanaged3\Debug\unmanaged3.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern void Foo(IntPtr lplf);

第三,您应该使用“fixed”关键字固定“TTest”,并将其指针传递给“Foo”。如果您正在使用类,则可以使用“Marhsal.StructureToPtr”从您的“TTest”获取“IntPtr”。
这提供了相同的功能,在两侧都可以传递任何类型的指针。您还可以编写所有要使用的类类型的重载,因为它们在本机端都等同于“void *”。对于结构体,您的参数将以“ref”开头。
我很好奇的是,为什么您的本机代码希望得到一个“void *”,而不是一个“TTest *”,当您在非托管代码中的第一件事就是将其转换为“TTest *”。如果您将参数切换为“TTest *”,那么提供相同的功能就变得更简单了。您的声明将变成:
[DllImport(@"C:\.net course\unmanaged1\unmanaged3\Debug\unmanaged3.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern void Foo(ref TTest lplf);

如果你要调用这个函数,需要使用Program.Foo(ref Test);

如果你在使用类,则不需要使用ref,因为类是引用类型。


所有这些更改都是完全不必要的,仍然未能解决 OP 实际遇到的错误(我猜测是堆栈不平衡)。 - ildjarn
@ildjarn 我在我的回答中添加了CallingConvention并删除了MarshalAs属性。这将解决即时问题。在错误得到修复之后,他会遇到另一个错误,其中他的“TTest”参数无法作为“void *”工作。 - Robert Rouhani
“在错误得到修复后,他会遇到另一个错误,其中他的TTest参数无法作为void*工作。” 不,他不会 - 对于C#,TTest *void *是100%相同的(因为TTest目前已定义)。 - ildjarn
1
再次强调,这完全是不必要的。;-] - ildjarn
1
即使是使用结构体,我认为您仍然可以使用 ref Test 而不是 IntPtr。因为 .NET 将从中创建 TTest* - 对于接收 Test* 还是 void* 作为参数的 C++(而不是 C#)而言,它是100%相同的。 - Krizz
显示剩余8条评论

1
您正在使用C调用,因此需要指定CallingConvention.Cdecl
[DllImport(@"C:\.net course\unmanaged1\unmanaged3\Debug\unmanaged3.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]

默认情况下,在C#中使用PInvoke是stdcall调用方式,我记得;您也可以更改C代码,使C#代码保持不变,就像下面的示例一样。
__declspec(dllexport) void __stdcall  Foo(void  *Test);

但对我而言,最好的方法是在C导出中同时声明__cdecl(或stdcall),并在C#代码中声明CallingConvention.Cdecl(或stdcall)以保持方便。您可以查看https://learn.microsoft.com/en-gb/cpp/cpp/argument-passing-and-naming-conventions?view=vs-2017https://learn.microsoft.com/en-gb/dotnet/api/system.runtime.interopservices.callingconvention?view=netframework-4.7.2获取更多信息。


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