使用C库的技巧:从C#调用

10
我有一个用C语言编写的库,希望能够在C#中使用。
根据我从互联网上搜集到的信息,一种方法 是将其封装在一个C++动态链接库中,并使用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++中重新编译?我在考虑,但看起来会很麻烦。

顺便说一下,这是一个有趣的问题!与C库进行接口有很多方法。如果我在你的位置上,可能会在运行库的C程序和你的.NET程序之间建立网络接口。但这取决于性能需求等因素。我以前从未听说过Reflection.emit,很酷的了解。@leppie - Prof. Falken
据我所知,Fortran DLL具有C接口,应该可以在没有Cpp包装器的情况下访问。但我不确定函数指针是否可行。 - Totonga
@Totonga,实际上它 确实 是一个 .lib,我原本以为我无法直接从 C# 中访问它。 - Benjol
@Benjol,如果一个库没有关联的dll,你就无法访问它。但是你可以使用def文件将库绑定到dll中,以导出所需的方法而无需重新编译代码。 - Totonga
6个回答

5
记录一下,我已经完成了大部分工作,然后将相应的C文件引入到C++项目中(因为我在处理不需要的代码兼容性问题)。
以下是我整理出来的一些提示,对于那些遇到这个问题的人可能会有帮助:

数组封送

在C语言中,指向double的指针(double*)和double数组(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中,而y则被“发送回来”。

调用约定

你实际上不需要理解这意味着什么更细节的内容(换句话说,我不需要),你只需要知道不同的调用约定存在,并且它们需要在两端匹配。C#的默认值是StdCall,C的默认值是Cdecl。这意味着如果与默认值不同,则只需要在使用它的一侧显式指定调用约定。

在回调的情况下,这特别棘手。如果我们从C#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

extern "C"

如果你想要在C++中使用C语言函数,你必须声明它们为extern "C"。如果你正在包含一个包含C函数的头文件,你需要这样做:

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

预编译头文件

为了避免编译错误,我需要进行另一个操作:右键单击 -> 属性 -> C/C++ -> 预编译头文件,并将每个'C'文件的预编译头文件设置为不使用预编译头文件


3
唯一的“nasty”参数是函数指针。但幸运的是,.NET通过委托很好地处理它们。
唯一的问题在于调用约定。在C#中,它只发出一个类型(iirc stdcall),而C代码可能会期望cdecl。然而,后者的问题可以在IL级别上处理(或使用Reflection.Emit)。 这里有一些代码可以通过Reflection.Emit来实现(这有助于理解需要放置在委托的Invoke方法上的伪属性)。

3

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 ++中包装,而且您可以获得所需的调用约定。

大多数设计用于在Windows中工作的C DLL使用stdcall调用约定,而不是cdecl - Ken White

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;
    }
}

1

几年前有一篇MSDN文章,导致了PInvoke Interop Assistant的出现。 这个小工具仍然对于调用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) ;

}

有一个新的改进工具包 - Fil

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