C# 对 C DLL 的调用只有部分功能

C# call to a C DLL is only partly functional

我正在通过用 C# 重写示例 C++/CLR 项目,从我的 C++/CLR 背景中学习 C#。

该项目是一个简单的 GUI(使用 Visual Studio/Windows 形式),它执行对用 C 编写的 DLL 的调用(实际上,在 NI LabWindows/CVI 中,但这只是带有自定义库的 ANSI C)。 DLL 不是我写的,我无法对其进行任何更改,因为它也在其他地方使用。

DLL 包含使 RFID 设备执行特定功能的函数(如 reading/writing RFID 标签等)。在这些函数中的每一个中,总是调用另一个执行日志文件写入的函数。如果日志文件不存在,则使用特定 header 创建它,然后附加数据。

问题是: C++/CLR 项目工作正常。 但是,在 C# 中,功能有效(RFID 标签是正确的 written/read 等)但是没有 activity 关于日志文件!

DLL 导出的声明如下所示(仅举一例,当然还有更多):

int __declspec(dllexport) __stdcall Magnetfeld_einschalten(char path_Logfile_RFID[300]);

int save_Logdatei(char path_Logdatei[], char Funktion[], char Mitteilung[]);   

save_Logdatei 函数在 Magnetfeld_einschalten 执行期间被调用,如下所示:

save_Logdatei(path_Logfile_RFID, "Magnetfeld_einschalten", "OK");

在C++/CLR项目中,我这样声明函数:

#ifdef __cplusplus
extern "C" {
#endif
int __declspec(dllexport) __stdcall Magnetfeld_einschalten(char path_Logfile_RFID[300]);
#ifdef __cplusplus
}
#endif

那么对函数的简单调用就可以了。

C#项目中,声明如下:

[DllImport("MyDLL.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "Magnetfeld_einschalten", CharSet = CharSet.Ansi, ExactSpelling = false)]

private static extern int Magnetfeld_einschalten(string path_Logfile_RFID);

并且,正如我所说,尽管主要功能正在运行(在本例中,打开 RFID 设备的磁场),但日志记录从未完成(因此,内部 DLL 调用 save_Logdatei 没有正确执行)。

Form构造函数中的相关代码如下:

pathapp = Application.StartupPath;
pathlog = string.Format("{0}\{1:yyyyMMdd}_RFID_Logdatei.dat", pathapp, DateTime.Now);
    //The naming scheme for the log file.
    //Normally, it's autogenerated when a `save_Logdatei' call is made.

Magnetfeld_einschalten(pathlog);

我错过了什么?我已经尝试使用 unsafe 作为 DLL 方法声明 - 因为 save_Logdatei 中有一个 File 指针 - 但它没有任何区别。

===================================编辑========= =========================

根据 David Heffernan 的建议,我尝试以一种易于测试的方式重现该问题。为此,我创建了一个非常简单的 DLL ("test.dll") 并且我已经将它从自定义 CVI 库中完全删除,因此即使没有 CVI 它也应该可以重现。我已经上传了here。不管怎样,DLL的代码是:

#include <stdio.h>

int __declspec(dllexport) __stdcall Magnetfeld_einschalten(char path_Logfile_RFID[300]);
int save_Logdatei(char path_Logdatei[], char Funktion[], char Mitteilung[]);  

int __declspec(dllexport) __stdcall Magnetfeld_einschalten(char path_Logfile_RFID[300])
{
    save_Logdatei(path_Logfile_RFID, "Opening Magnet Field", "Success");
    return 0;
}

int save_Logdatei(char path_Logdatei[], char Funktion[], char Mitteilung[])
{
    FILE    *fp;                                /* File-Pointer */
    char    line[700];                          /* Zeilenbuffer */
    char    path[700];

    sprintf(path,"%s\20160212_RFID_Logdatei.dat",path_Logdatei);

    fp = fopen (path, "a");

    sprintf(line, "Just testing");
    sprintf(line,"%s    %s",line, Funktion); 
    sprintf(line,"%s    %s",line, Mitteilung);

    fprintf(fp,"%s\n",line);

    fclose(fp);
    return 0;
}

C# 代码也被精简了,我唯一添加到标准 Forms 项目的是 Button 1(以及生成的按钮点击,如图所示)。代码是这样的:

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace TestDLLCallCSharp
{
    public partial class Form1 : Form
    {
        public int ret;
        public string pathapp;
        public string pathlog;

        [DllImport("test", CallingConvention = CallingConvention.StdCall, EntryPoint = "Magnetfeld_einschalten", CharSet = CharSet.Ansi, ExactSpelling = false)]
        private static extern int Magnetfeld_einschalten(string path_Logfile_RFID);

        public Form1()
        {
            pathapp = @"C:\ProgramData\test";
            pathlog = string.Format("{0}\20160212_RFID_Logdatei.dat", pathapp);

            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
        }

        private void button1_Click(object sender, EventArgs e)
        {
            ret = Magnetfeld_einschalten(pathlog);
        }
    }
}

可以看出,我避免对日志文件使用自动命名方案(通常我使用日期),并且在 dll 和 C# 代码中,日志文件都是“20160212_RFID_Logdatei.dat” .我也避免使用应用程序路径作为放置日志文件的目录,而是选择了我在 ProgramData

中创建的名为 test 的文件夹

同样,根本没有创建任何文件

字符串为Unicode格式,转换为byte[]

Encoding ec = Encoding.GetEncoding(System.Threading.Thread.CurrentThread.CurrentCulture.TextInfo.ANSICodePage);
byte[] bpathlog = ec.GetBytes(pathlog);

并将参数类型更改为 byte[]

[DllImport("MyDLL.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "Magnetfeld_einschalten", CharSet = CharSet.Ansi, ExactSpelling = false)]    
private static extern int Magnetfeld_einschalten(byte[] path_Logfile_RFID);

对我来说很有效

JSh

Platform Invoke Tutorial - MSDN Library.

中给出了 C# 中 P/Invoke 的广泛概述

问题在于您需要传递一个固定的 char 数组而不是标准的 char*Default Marshalling for Strings.

中对此进行了介绍

要点是,您需要从 C# 字符串构造一个 char[300] 并传递它而不是字符串。

对于这种情况,指定了两种方式:

  • pass a StringBuilder instead of a string initialized to the specified length and with your data (I omitted non-essential parameters):

    [DllImport("MyDLL.dll", ExactSpelling = true)]
    private static extern int Magnetfeld_einschalten(
      [MarshalAs(UnmanagedType.LPStr)] StringBuilder path_Logfile_RFID);
    <...>
    StringBuilder sb = new StringBuilder(pathlog,300);
    int result = Magnetfeld_einschalten(sb);
    

    在这种情况下,缓冲区是可修改的。

  • define a struct with the required format 并手动将您的字符串转换为它:

    [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
    struct Char300 {
        [MarshalAs(UnmanagedType.ByValTStr,SizeConst=300)]String s;
    }
    [DllImport("MyDLL.dll")]
    private static extern int Magnetfeld_einschalten(Char300 path_Logfile_RFID);
    <...>
    int result = Magnetfeld_einschalten(new Char300{s=pathlog});
    

    您可以define an explicit or implicit cast routine使这更直接。

根据 UnmanagedType docsUnmanagedType.ByValTStr 仅在结构中有效,因此似乎不可能两全其美。

这看起来像是您的调用代码中的一个简单拼写错误。而不是:

ret = Magnetfeld_einschalten(pathlog);

你的意思是写:

ret = Magnetfeld_einschalten(pathapp);

在C#代码中,这两个字符串的值如下:

pathapp == "C:\ProgramData\test"
pathlog == "C:\ProgramData\test\20160212_RFID_Logdatei.dat"

当您将 pathlog 传递给非托管代码时,它会执行以下操作:

sprintf(path,"%s\20160212_RFID_Logdatei.dat",path_Logdatei);

path设置为

path == "C:\ProgramData\test\20160212_RFID_Logdatei.dat\20160212_RFID_Logdatei.dat"

换句话说,您将文件名附加到路径中两次,而不是一次。