从C#调用C++函数 - 不平衡的堆栈

4

我有一个未管理的C++函数,签名如下:

int function(char* param, int ret)

我将尝试从 C# 中调用它:
unsafe delegate int MyFunc(char* param, int ret);

...

int Module = LoadLibrary("fullpathToUnamanagedDll");
IntPtr pProc = GetProcAddress(Module, "functionName");
MyFunc func = (MyFunc)System.Runtime.InteropServices.Marshal.GetDelegateForFunctionPointer(pProc, typeof(MyFunc));

unsafe
{
    char* param = null;
    int ret = 0;
    int result = func(param, ret);
}

据我从旧的C++项目规格说明中了解到,对于函数来说,null作为参数和0作为返回值都是有效的输入。当我尝试调用它时,似乎可以正常工作,但是在退出时,我会得到以下错误:

检测到PInvokeStackImbalance

调用PInvoke函数'...::Invoke'导致堆栈不平衡。这可能是因为托管PInvoke签名与非托管目标签名不匹配。请检查PInvoke签名的调用约定和参数是否与目标非托管签名匹配。

我已经尝试了几乎所有我能想到的方法(unsafe是最后的手段),但是我无法找到任何一种方法可以运行函数而不会导致堆栈不平衡。还有其他什么方法可以尝试吗?

你必须将它作为委托导入吗?一个普通的 static extern 声明会更容易些。 - H H
@Henk Holterman:我猜不是这样的,我是从一个同事那里得到这段代码的,我认为他有他这样做的原因。然而,在运行时动态加载函数时,我不知道Dll名称(路径)或函数名称。但是,我在设计时确实知道函数签名。是否可以以不同的方式完成? - David Božjak
“动态地”是一个很好的理由,我认为@leppies的答案是你需要去的地方。 - H H
3个回答

8

我知道这个问题已经有一年了,但是比起动态构建类型,一个更简单的方法是在你的委托上使用UnmanagedFunctionPointer属性声明调用约定,像这样:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)] 
unsafe delegate int MyFunc(char* param, int ret);

来自MSDN

控制将委托签名作为非托管函数指针传递给或从非托管代码返回时的编组行为。


1
+1:谢谢,这个问题已经被解决了。我有一个 typedef void (*InvokeCallack)(void *opaque); 回调定义,实际的回调是一个 C++ 编译方法。我在 C++/CLI 中定义了一个带有签名 delegate void InvokeCallack(IntPtr opaque); 的委托,并使用您的属性 [UnmanagedFunctionPointer(CallingConvention::Cdecl)],现在我可以在 UI 线程中使用 Control::BeginInvoke 调用委托而不会出现问题。奇怪的是,在 VS2008 中相同的代码可以工作而不需要该属性。在 VS2010 中需要该属性。 - ceztko

2
据我所知,您需要使用调用约定来装饰委托签名。不幸的是,这只能通过IL或使用Reflection.Emit生成存根来完成。
您可以尝试以下方法:
protected static Type MakeDelegateType(Type returntype, List<Type> paramtypes)
{
  ModuleBuilder dynamicMod = ... ; // supply this

  TypeBuilder tb = dynamicMod.DefineType("delegate-maker" + Guid.NewGuid(), 
      TypeAttributes.Public | TypeAttributes.Sealed, typeof(MulticastDelegate));

  tb.DefineConstructor(MethodAttributes.RTSpecialName | 
       MethodAttributes.SpecialName | MethodAttributes.Public |
       MethodAttributes.HideBySig, CallingConventions.Standard,
       new Type[] { typeof(object), typeof(IntPtr) }). 
       SetImplementationFlags(MethodImplAttributes.Runtime);

  var inv = tb.DefineMethod("Invoke", MethodAttributes.Public | 
       MethodAttributes.Virtual | MethodAttributes.NewSlot | 
       MethodAttributes.HideBySig, 
       CallingConventions.Standard ,returntype,null, 
       new Type[] 
       { 
          // this is the important bit
          typeof(System.Runtime.CompilerServices.CallConvCdecl)
       }, 
       paramtypes.ToArray(), null, null);

  inv.SetImplementationFlags(MethodImplAttributes.Runtime);

  var t = tb.CreateType();
  return t;
}

很抱歉,我需要一些帮助来理解这段代码。我的Dll文件(或其路径)和函数名应该放在哪里? - David Božjak
@Rekreativc:就像你所做的那样。我的方法返回函数指针时会发送结果。也就是说,它将是Delegate类型,您需要在其上调用DynamicInvoke。您可以创建另一个Reflection.Emit包装器来直接调用它。 - leppie
1
@Rekreativc:是的,然后在结果上调用“DynamicInvoke”。 - leppie
我也遇到了同样的问题,由于我是C#的新手,所以无法理解如何使用你的答案(如何将调用dll函数、获取结果参数与你编写的代码结合起来)。如果你能在你的答案中将所有这些内容结合起来就太好了。 - DanielHsH

-1

有两件事需要注意:C#位和您的DLL之间的调用约定以及char *数据如何跨越此接口进行编组。如果您其中任何一个出错,那么您将收到堆栈损坏的投诉。在定义接口时,如果您可以将数据块的大小限制为某个固定值,即设置最大字符串长度,则会更容易。

这是静态版本,其中DLL名称固定,您的字符串被处理为byte []并且大小限制为2Kbyte,您可以从中找出动态版本:

    private const string MYDLL = @"my.dll";

    [DllImport(MYDLL, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Auto)]
    public static extern int DataBlockDownload([MarshalAs(UnmanagedType.U4)] int A, [MarshalAs(UnmanagedType.LPArray, SizeConst = 2048)] byte[] B, [MarshalAs(UnmanagedType.U4)] int C);

    // NOTE: The data block byte array is fixed at 2Kbyte long!!
public delegate int DDataBlockCallback([MarshalAs(UnmanagedType.U4)] int A, [MarshalAs(UnmanagedType.LPArray, SizeConst = 2048)] byte[] B, [MarshalAs(UnmanagedType.U4)] int C);

如果您想继续使用char类型,您可能还需要指定您正在使用的字符集。

您没有说明您正在如何处理char *数据,它是作为参数传递给C++代码还是C++代码将其传回托管世界。请阅读有关C#关键字ref和out的文章,以避免使用char *类型和unsafe修饰符。

通过一些谷歌搜索,您应该能够找到答案。


问题在于你无法在C#中为委托指定调用约定,这就是导致问题的原因。 - leppie

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