9

我有一个 C 语言库,我想从 C# 中使用它。

根据我从网上收集到的信息,一个想法是将其包装在 C++ dll 中,然后 DllImport 将其包装起来。

问题是我要调用的函数有一个相当讨厌的参数集:包括对函数的引用(将是一个 .NET 函数)和几个数组(一些写入,一些读取)。

int lmdif(int (*fcn)(int, int, double*, double*, int*), 
          int m, int n, double* x, double ftol, double xtol, double gtol, 
          int maxfev, double epsfcn, double factor)

给定这样的界面,我应该注意什么?(请也提供解决方案)

你为什么不...

- 用 C# 重写?我开始了,但它已经是从 FORTRAN 机器翻译的,我不太喜欢编码我无法理解的东西。

- 使用现有的 .NET 库?我现在正在尝试,但结果并不完全相同

- 在 C++ 中重新编译?我在想,但看起来很痛苦

4

6 回答 6

4

作为记录,我得到了大部分的方法来让它工作,然后最终将适当的 C 文件拉入 C++ 项目(因为我正在与我什至不需要的代码中的兼容性问题作斗争)。

以下是我在此过程中收集的一些提示,可能对遇到此问题的人有所帮助:

编组阵列

double*在 C 中,指向双精度 ( ) 的指针和双精度数组( )之间没有区别double*。当你来到互操作时,你需要能够消除歧义。我需要传递 的数组double,因此签名可能如下所示:

[DllImport(@"PathToDll")]
public static extern Foo(
    [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)[In] double[] x, 
    [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)[Out] double[] y, 
    int x_size, 
    int y_size)

C 需要有关数组长度的额外信息,您必须将其作为单独的参数传入。编组还需要知道这个大小在哪里,因此您指定SizeParamIndex,它表示参数列表中大小参数的从零开始的索引。

您还可以指定要传递数组的方向。在这个例子x中被传递 Foo'sends back'y中。

调用约定

您实际上不需要了解这意味着什么的更详细的细节(换句话说,我不需要),您只需要知道存在不同的调用约定,并且它们需要在双方都匹配。C# 默认为StdCall,C 默认为Cdecl. 这意味着您只需要显式指定调用约定,如果它与您使用它的任何一方的默认值不同。

这在回调的情况下尤其麻烦。如果我们将回调传递给Cfrom C#,我们打算使用 调用该回调StdCall,但是当我们传入它时,我们正在使用Cdecl. 这会产生以下签名(有关上下文,请参阅此问题):

//=======C-code======
//type signature of callback function
typedef int (__stdcall *FuncCallBack)(int, int);

void SetWrappedCallback(FuncCallBack); //here default = __cdecl

//======C# code======
public delegate int FuncCallBack(int a, int b);   // here default = StdCall 

[DllImport(@"PathToDll", CallingConvention = CallingConvention.Cdecl)]
private static extern void SetWrappedCallback(FuncCallBack func);

打包回调

很明显,但对我来说并不是很明显:

int MyFunc(int a, int b)
{
   return a * b;
}
//...

FuncCallBack ptr = new FuncCallBack(MyFunc);
SetWrappedCallback(ptr);

.def 文件

您想从 C++ 项目(待DllImport编辑)公开的任何函数都需要在ModDef.def文件中显示,其内容如下所示:

LIBRARY MyLibName
EXPORTS
    SetWrappedCallback  @1

外部“C”

如果你想在 C++ 中使用 C 函数,你必须将它们声明为extern "C". 如果你包含 C 函数的头文件,你可以这样:

extern "C" {
  #include "C_declarations.h"
}

预编译头文件

为了避免编译错误,我必须做的另一件事是将每个“C”文件Right-click -> Properties -> C/C++ -> Precompiled Headers设置Precompiled header不使用预编译头文件。

于 2012-06-05T11:32:26.790 回答
3

唯一的“讨厌”参数是函数指针。但幸运的是,.NET 通过委托很好地处理了它们。

唯一的问题是调用约定。在 C# 中,它只发出一种类型 (iirc stdcall),而 C 代码可能期望cdecl. 后一个问题可以在 IL 级别上处理(或使用Reflection.Emit)。

这是一些通过它执行的代码Reflection.Emit(这是为了帮助理解需要在委托的方法上放置什么伪属性Invoke)。

于 2012-06-01T09:07:54.397 回答
3

Marshal.GetFunctionPointerForDelegate的 Microsoft 文档:

“将委托转换为可从非托管代码调用的函数指针。”

于 2012-06-01T09:30:10.800 回答
3

这就是我通常与 C# 中的 C DLL 交互的方式:

  public static unsafe class WrapCDll {
    private const string DllName = "c_functions.dll";

    [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
    public static extern void do_stuff_in_c(byte* arg);
  }

无需使用 C++ 包装,您就可以获得所需的调用约定。

于 2012-06-01T15:59:34.077 回答
1

几年前有一篇MSDN 文章引用了InteropSignatureToolkit。这个小工具对于编组 C 接口仍然很有用。将接口代码复制并粘贴到“SigImp Translation Sniplet”中并观察结果。

结果如下,但我不知道如何使用委托或它是否有效。因此,如果它有效,请添加一些评论。

/// Return Type: int
///param0: int
///param1: int
///param2: double*
///param3: double*
///param4: int*
public delegate int Anonymous_83fd32fd_91ee_45ce_b7e9_b7d886d2eb38(int param0, int param1, ref double param2, ref double param3, ref int param4);

public partial class NativeMethods {

    /// Return Type: int
    ///fcn: Anonymous_83fd32fd_91ee_45ce_b7e9_b7d886d2eb38
    ///m: int
    ///n: int
    ///x: double*
    ///ftol: double
    ///xtol: double
    ///gtol: double
    ///maxfev: int
    ///epsfcn: double
    ///factor: double
    [System.Runtime.InteropServices.DllImportAttribute("<Unknown>", EntryPoint="lmdif", CallingConvention=System.Runtime.InteropServices.CallingConvention.Cdecl)]
public static extern  int lmdif(Anonymous_83fd32fd_91ee_45ce_b7e9_b7d886d2eb38 fcn, int m, int n, ref double x, double ftol, double xtol, double gtol, int maxfev, double epsfcn, double factor) ;

}
于 2012-06-01T17:59:22.553 回答
1

这是一种通过 StringBuilder 将大量数字数据发送到 C 函数的方法。只需将您的数字转储到 StringBuilder 中,同时在适当的位置设置分隔符等等:

class Program
    {  
        [DllImport("namEm.DLL", CallingConvention = CallingConvention.Cdecl, EntryPoint = "nameEm", 
            CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
        public static extern int nameEm([MarshalAs(UnmanagedType.LPStr)] StringBuilder str);
        static void Main(string[] args)
        {
            int m = 3;
            StringBuilder str = new StringBuilder();
            str.Append(String.Format("{0};", m));
            str.Append(String.Format("{0} {1:E4};", 5, 76.334E-3 ));
            str.Append(String.Format("{0} {1} {2} {3};", 65,45,23,12));
            m = nameEm(str);
        }
    }

在 C 端,将 StringBuilder 选为 char*:

extern "C"
{
    __declspec(dllexport) int __cdecl nameEm(char* names)
    {
        int n1, n2, n3[4];
        char *token,
             *next_token2 = NULL,
             *next_token = NULL;
        float f;

        sscanf_s(strtok_s(names, ";", &next_token), "%d", &n2);
        sscanf_s(strtok_s(NULL, ";", &next_token), "%d %f", &n1, &f);
        // Observe the use of two different strtok-delimiters.
        // the ';' to pick the sequence of 4 integers,
        // and the space to split that same sequence into 4 integers.
        token = strtok_s(strtok_s(NULL, ";", &next_token)," ",&next_token2);
        for (int i=0; i < 4; i++)
        {
             sscanf_s(token,"%d", &n3[i]);
             token = strtok_s(NULL, " ",&next_token2);
        }    
        return 0;
    }
}
于 2016-07-12T12:57:07.123 回答