2

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

Community
  • 1
  • 1
IKO
  • 23
  • 5
  • Have you tried to add the `save_Logdatei`-method with DllImport and then excecute it directly? Does it work this way? – Claudio P Feb 11 '16 at 14:40
  • At first, I thought it was a problem with marshaling a C# `string` to a fixed-length `char` array. I did some experiments on my machine, though, and didn't have a problem. You could try experimenting with the `MarshalAs` attribute (on the `path_Logfile_RFID` parameter) to see if that helps. – qxn Feb 11 '16 at 14:50
  • 2
    You are not missing anything. Using Application.StartupPath to create files is a pretty bad idea, that normally will produce a path that is not writable. Apps don't have write access to C:\Program Files for example. That C++ code won't tell you that it failed to create the file is quite normal. You can see it with SysInternals' Process Monitor. Use AppData instead. – Hans Passant Feb 11 '16 at 14:50
  • @ClaudioP I tried that and it still doesn't work – IKO Feb 11 '16 at 15:16
  • @HansPassant I don't think this is the case here. Thanks for enlightening me about the constraints though and i will keep it in mind but in this case it made no difference removing the Application.StartupPath and replacing it with just the address of the folder of the executable directly. (that way pathlog would be defined explicitly, as it is merely forming the correct file ending and adding it to an explicit address) – IKO Feb 11 '16 at 16:57
  • I would suggest to break into the DLL function (the one called directly from C#) and see what exactly it is getting. – ivan_pozdeev Feb 12 '16 at 03:12
  • It looks like you are still trying to write to the application's executable directory, if so you will still have issues. try using the ProgramData directory instead. – Mark Hall Feb 12 '16 at 03:15
  • The question is off topic because you didn't provide the details we need to answer. We don't even know which way the data flows. That your C++ code seems allergic to the use of const doesn't help us understand. Guessing is no fun. – David Heffernan Feb 12 '16 at 07:09
  • @MarkHall You are right, my mistake. I tried the ProgramData directory route but again there is no difference – IKO Feb 12 '16 at 07:59
  • @DavidHeffernan What info do you need? – IKO Feb 12 '16 at 08:09
  • A [mcve] is what we need – David Heffernan Feb 12 '16 at 08:09
  • @DavidHeffernan Ok, let me see what i can do – IKO Feb 12 '16 at 08:13

3 Answers3

3

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.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Jesus Christ, i can't believe I wasted the time of all of you for this.. Some times you miss the obvious things, it seems. Thanks for your help and sorry for that! – IKO Feb 12 '16 at 11:17
  • Yeah, it's easy to assume that the complicated things are what are killing you. Some good old fashioned debugging would have done the trick. Output the arguments and intermediate values to the console from the C++ code to check that the expected values were appearing. – David Heffernan Feb 12 '16 at 11:23
  • Will do from now on, would have saved me a lot of time.. Thanks a lot David and also for pointing me to the correct guidelines for posting! – IKO Feb 12 '16 at 11:31
  • You are very welcome. It's very nice to come across an asker so eager to learn. Not all are like you in that regard. I approve! – David Heffernan Feb 12 '16 at 11:34
  • Shucks. This means this is gonna be closed as "trivial error", and all that investigative work was for nothing. – ivan_pozdeev Feb 12 '16 at 23:42
0

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

ardila
  • 1,277
  • 1
  • 13
  • 24
JurisSh
  • 34
  • 3
  • Hi JurisSh, i tried that but I get: An unhandled exception of type 'System.AccessViolationException' occurred in ContrinexCSharp.exe Additional information: Attempted to read or write protected memory. This is often an indication that other memory is corrupt. – IKO Feb 11 '16 at 15:10
  • Hi IKO, about previous- I located the only difference in my DLL the parameter is char * type, not char[300]. I don't known is it important or not. About Ken comment above. I located the different string transmition to DLL in my project: [DllImport("winscard.dll", SetLastError = true, CharSet = CharSet.Auto)] internal static extern int SCardConnect(IntPtr /*UInt32*/ hContext, [MarshalAs(UnmanagedType.LPTStr)] string szReader, UInt32 dwShareMode, UInt32 dwPreferredProtocols, IntPtr phCard, IntPtr pdwActiveProtocol); my be it can help.... Regards, JSh – JurisSh Feb 11 '16 at 17:15
0

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.

ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152