从C#使用C库的提示

我在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 ++重新编译? 我正在考虑它,但它看起来很痛苦

唯一的’讨厌’参数是函数指针。 但幸运的是.NET通过代理处理它们。

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

下面是一些通过Reflection.Emit此操作的代码 (这有助于了解需要在委托的Invoke方法中放置什么样的psuedo属性)。

Marshal.GetFunctionPointerForDelegate的 Microsoft文档:

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

这就是我通常与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 ++中包装,您就可以获得所需的调用约定。

为了记录,我得到了大部分工作,然后最终将适当的C文件拉入C ++项目(因为我正在解决代码中的兼容性问题,我甚至不需要)。

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

编组arrays

在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 ,它指示参数列表中size参数的从零开始的索引。

您还可以指定要传递数组的方向。 在这个例子中, 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 

外部“C”

如果要使用C ++中的C函数,则必须将它们声明为extern "C" 。 如果你包含C函数的头文件,你会这样:

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

预编译的头文件

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

几年前有一篇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("", 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) ; } 

这是一种通过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; } }