使用 C# 中的 C 库的提示
本文关键字:提示 中的 使用 | 更新日期: 2023-09-27 18:35:54
我有一个 C 语言库,我想从 C# 中使用它。
从我从互联网上收集到的信息来看,一个想法是将其包装在C++ dll中,然后Dll导入。
问题是我想调用的函数有一个相当讨厌的参数集:包括对函数(这将是 .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++项目中(因为我正在与我甚至不需要的代码中的兼容性问题作斗争)。
以下是我在此过程中获得的一些提示,可能对遇到此问题的人有所帮助:
编组阵列
在 C 中,指向双精度 ( 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
外部"C"
如果要使用 C++ 中的 C 函数,则必须将它们声明为 extern "C"
。如果你包含 C 函数的头文件,你可以这样:
extern "C" {
#include "C_declarations.h"
}
预编译标头
为了避免编译错误,我必须做的另一件事是Right-click -> Properties -> C/C++ -> Precompiled Headers
并将Precompiled header
设置为不使用每个"C"文件的预编译头。
唯一的"讨厌"参数是函数指针。但幸运的是,.NET 通过委托很好地处理了它们。
唯一的问题是调用约定。在 C# 中,它只发出一种类型(iirc stdcall
),而 C 代码可能需要cdecl
。后一个问题可以在 IL 级别(或使用 Reflection.Emit
)处理。
下面是一些通过 Reflection.Emit
执行此操作的代码(这有助于了解需要在委托的 Invoke
方法上放置哪些伪属性)。
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++,您将获得所需的调用约定。
这是一种通过 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;
}
}
几年前有一篇MSDN文章导致了PInvoke Interop Assistant。这个小工具对于编组 C 接口仍然很有用。将界面代码复制并粘贴到"SigImp 翻译截图"中并观察结果。
结果如下,但我不知道如何使用委托或它是否有效。因此,如果它有效,请添加一些评论。
/// 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) ;
}