IKO
IKO

Reputation: 23

C# call to a C DLL is only partly functional

I am learning C# from my C++/CLR background by rewriting a sample C++/CLR project in C#.

The project is a simple GUI (using Visual Studio/ Windows Forms) that performs calls to a DLL written in C (in fact, in NI LabWindows/CVI but this is just ANSI C with custom libraries). The DLL is not written by me and I cannot perform any changes to it because it is also used elsewhere.

The DLL contains functions to make an RFID device perform certain functions (like reading/writing RFID tag etc). In each of these functions, there is always a call to another function that performs writing to a log file. If the log file is not present, it is created with a certain header and then data is appended.

The problem is: the C++/CLR project works fine. But, in the C# one, the functions work (the RFID tag is correctly written/read etc.) but there is no activity regarding the log file!

The declarations for DLL exports look like this (just one example, there are more of them, of course):

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

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

The save_Logdatei function is called during execution of Magnetfeld_einschalten like this:

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

In the C++/CLR project, I declared the function like this:

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

then a simple call to the function is working.

In the C# project, the declaration goes like:

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

private static extern int Magnetfeld_einschalten(string path_Logfile_RFID);

and, as I said, although the primary function is working (in this case, turning on the magnetic field of the RFID device), the logging is never done (so, the internal DLL call to save_Logdatei is not executing correctly).

The relevant code in the Form constructor is the following:

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

What am I missing? I have already tried using unsafe for the DLL method declaration - since there is a File pointer in save_Logdatei - but it didn't make any difference.

===================================EDIT==================================

Per David Heffernan's suggestion, i have tried to recreate the problem in an easy to test way. For this, i have created a very simple DLL ("test.dll") and I have stripped it completely from the custom CVI libaries, so it should be reproducible even without CVI. I have uploaded it here. In any case, the code of the DLL is:

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

The C# code is also stripped down and the only thing i have added to the standard Forms project, is Button 1 (and the generated button click as can be seen). The code is this:

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

As can be seen, I have avoided using an automatic naming scheme for the log file (normally i use the date) and in both the dll and the C# code, the log file is "20160212_RFID_Logdatei.dat". I have also avoided using the app path as the directory where to put the log file and instead I have opted for a folder named test i created in ProgramData

Again, no file is created at all

Upvotes: 2

Views: 352

Answers (3)

JurisSh
JurisSh

Reputation: 34

The String is in Unicode format, convert it to byte[]

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

and change parameter type to 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);

For me it is working

JSh

Upvotes: 0

David Heffernan
David Heffernan

Reputation: 613582

This looks like a simple typo in your calling code. Instead of:

ret = Magnetfeld_einschalten(pathlog);

you mean to write:

ret = Magnetfeld_einschalten(pathapp);

In the C# code, these two strings have the following values:

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

When you pass pathlog to the unmanaged code it then does the following:

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

which sets path to be

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

In other words you are appending the file name to the path twice instead of once.

Upvotes: 3

ivan_pozdeev
ivan_pozdeev

Reputation: 36116

An extensive overview for P/Invoke in C# is given in Platform Invoke Tutorial - MSDN Library.

The problematic bit is you need to pass a fixed char array rather than the standard char*. This is covered in Default Marshalling for Strings.

The gist is, you need to construct a char[300] from your C# string and pass that rather than the string.

For this case, two ways are specified:

  • 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);
    

    In this case, the buffer is modifiable.

  • define a struct with the required format and manually convert your string to it:

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

    You can define an explicit or implicit cast routine to make this more straightforward.

According to UnmanagedType docs, UnmanagedType.ByValTStr is only valid in structures so it appears to be impossible to get the best of both worlds.

Upvotes: 0

Related Questions