How do I properly import functions from a C++ DLL into my C# Project

半腔热情 提交于 2019-12-04 07:15:36

问题


I could use help with how to properly import several functions from a C++ DLL into my C# application. Here are a couple examples from the C++ side that show what I am trying to do in C#. I do not think I am properly marshaling either/or some of the return types and some of the parameters (especially pointers/ref/out).

C++ Header File Declarations:

unsigned long __stdcall mfcsez_initialisation(unsigned short serial);

unsigned char __stdcall mfcs_get_serial(unsigned long int handle,
                                        unsigned short * serial);

unsigned char __stdcall mfcs_read_chan(unsigned long int handle,
                                       unsigned char canal,
                                       float * pressure,
                                       unsigned short * chrono);

C++ Code:

/* Define functions prototype */
typedef unsigned long(__stdcall *init)(int);

typedef unsigned char(__stdcall *serial)(unsigned long handle, unsigned 
                                         short *serial);

typedef unsigned char(__stdcall *readChannel)(unsigned long handle, 
                                              unsigned char chan, 
                                              float * pressure, 
                                              unsigned short * chrono);

int main(int argc, char *argv[])
{
     unsigned char pressureChannel = 1; 
     HINSTANCE hGetProcIDDLL=NULL;

     /* Load DLL into memory */
     hGetProcIDDLL = LoadLibrary(TEXT("mfcs64_c.dll"));

     /* Declare pointers on dll functions */
     init dll_init;
     serial dll_serial;
     readChannel dll_readChannel;

     /* Link dll pointers with functions prototype */
     dll_init = (init)GetProcAddress(hGetProcIDDLL, 
                                     "mfcsez_initialisation");
     dll_serial = (serial)GetProcAddress(hGetProcIDDLL, 
                                         "mfcs_get_serial");
     dll_readChannel = (readChannel)GetProcAddress(hGetProcIDDLL, 
                                                   "mfcs_read_chan");

     /* Define variables used for MFCS device */
     unsigned long mfcsHandle;
     unsigned short mySerial;
     float read_pressure;
     unsigned short chrono;
     int loop_index;


     if (hGetProcIDDLL != NULL) 
     {        
         std::cout << "mfcs_c.dll is loaded" << std::endl;

         /* Initialize device */
         if (dll_init != NULL) 
         {         
             /* Initialize the first MFCS in Windows enumeration list */
             mfcsHandle = dll_init(0);
         }

          /* Read device serial number */
          dll_serial(mfcsHandle, &mySerial);

          for (loop_index = int(start_pressure); 
               loop_index<target_pressure; loop_index++)
          {
               Sleep(1000);                                                                      
               dll_readChannel(mfcsHandle, pressureChannel, 
                               &read_pressure, &chrono);
          }
     }

    return EXIT_SUCCESS;
}

I have tried importing them with various footprints. I am able to call mfcsez_initialisation and it works just fine as imported below. The other two I have tried many different ways and always get an exception - either from the DLL (unrecoverable) or from improper marshalling which I can try/catch.

Example of C# Import Statements:

    [DllImport("mfcs_c_64.dll", CallingConvention = 
               CallingConvention.StdCall)]
    protected static unsafe extern uint mfcsez_initialisation(ushort                         
                                                        serial_number);

    [DllImport("mfcs_c_64.dll", CallingConvention = 
               CallingConvention.StdCall)]
    public static unsafe extern byte mfcs_get_serial(uint handle, ref 
                                                     ushort serial);

    [DllImport("mfcs_c_64.dll", CallingConvention = 
               CallingConvention.StdCall)]
    protected static unsafe extern byte mfcs_read_chan(ulong handle, byte 
                          canal, ref float pressure, ref ushort chrono);

Example of C# Code:

unit mfcsHandle = mfcsez_initialisation(0);  // Returns with valid handle

mfcs_get_serial(mfcsHandle, mySerial);  // Memory write exception

float pressure = -1.0f;
ushort chrono = 0;
mfcs_read_chan(mfcsHandle, 1, ref pressure, ref chrono);  // Same ex

Any and all help is appreciated!


回答1:


As you have stated in comments (subsequently deleted), you can't be sure whether the problem lies in the interop or the parameters passed to the function. How are you going to resolve that doubt?

The way to do that is to create a test bed DLL that has functions with the same signatures, and then prove that you can move data correctly between that DLL and your C# p/invoke code. Once you can do that you can remove interop as a potential source of your problem, and concentrate on the parameters passed to the function. So, here is what is needed to make that test bed DLL.

dllmain.cpp

#include <Windows.h>

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Dll1.cpp

#include <iostream>

extern "C"

{
    unsigned long __stdcall mfcsez_initialisation(unsigned short serial)
    {
        std::cout << "mfcsez_initialisation, " << serial << std::endl;
        return 1;
    }

    unsigned char __stdcall mfcs_get_serial(unsigned long int handle,
        unsigned short * serial)
    {
        std::cout << "mfcs_get_serial, " << handle << std::endl;
        *serial = 2;
        return 3;
    }

    unsigned char __stdcall mfcs_read_chan(unsigned long int handle,
        unsigned char canal,
        float * pressure,
        unsigned short * chrono)
    {
        std::cout << "mfcs_read_chan, " << handle << ", " << static_cast<int>(canal) << std::endl;
        *pressure = 4.5f;
        *chrono = 5;
        return 6;
    }

}

Dll1.def

LIBRARY   Dll1
EXPORTS  
   mfcsez_initialisation  
   mfcs_get_serial
   mfcs_read_chan

Note that I am using a .def file to ensure that functions are exported using their undecorated names.

The C# program that calls this looks like so:

Program1.cs

using System;
using System.Runtime.InteropServices;
using System.Text;

namespace ConsoleApp1
{
    class Program
    {
        const string dllname = "Dll1.dll";

        [DllImport(dllname, CallingConvention = CallingConvention.StdCall)]
        static extern uint mfcsez_initialisation(ushort serial);

        [DllImport(dllname, CallingConvention = CallingConvention.StdCall)]
        static extern byte mfcs_get_serial(uint handle, out ushort serial);

        [DllImport(dllname, CallingConvention = CallingConvention.StdCall)]
        static extern byte mfcs_read_chan(uint handle, byte canal, out float pressure, out ushort chrono);

        static void Main(string[] args)
        {
            uint retval1 = mfcsez_initialisation(11);
            Console.WriteLine("return value = " + retval1.ToString());
            Console.WriteLine();

            ushort serial;
            byte retval2 = mfcs_get_serial(12, out serial);
            Console.WriteLine("serial = " + serial.ToString());
            Console.WriteLine("return value = " + retval2.ToString());
            Console.WriteLine();

            float pressure;
            ushort chrono;
            byte retval3 = mfcs_read_chan(13, 14, out pressure, out chrono);
            Console.WriteLine("pressure = " + pressure.ToString());
            Console.WriteLine("chrono = " + chrono.ToString());
            Console.WriteLine("return value = " + retval3.ToString());

            Console.ReadLine();
        }
    }
}

The output is:

mfcsez_initialisation, 11
return value = 1

mfcs_get_serial, 12
serial = 2
return value = 3

mfcs_read_chan, 13, 14
pressure = 4.5
chrono = 5
return value = 6

As you can see, all the desired values travel between the two modules correctly. This demonstrates that the p/invoke interop code here is correct.

Notes:

  1. In C++ on windows, both int and long int are 32 bit types. They therefore map to int on C#, oruint` for unsigned variants.
  2. On C#, long and ulong are 64 bit types, and so do not match C++ int or long int`.
  3. Map unsigned char on C++ to byte on C#.
  4. There is no need for unsafe here. You would use unsafe if you needed to use pointers, but you don't, and seldom do.
  5. I used out rather than ref because I infer that these parameters are only used for data flowing out of the DLL.

If you use this interop code against your DLL and still encounter failure then there are two plausible explanations:

  1. The parameters you are passing to the DLL are incorrect.
  2. The DLL is not what you think it is. Perhaps you have a version of the DLL which was built against different header files.



回答2:


It depends what this DLL is besides written in C++.

If it is a C++ .NET DLL, you can use it like any other .NET DLL. And like the ones you already use provided by the Framework.

If it was written with .NET's predecessor COM in mind, you can use COM interop. Backwards compatibiltiy was thought about when making .NET

If it is neither of those, there is P/Invoke.

Note that COM interop and P/Invoke usually involve handling naked pointers. That means binarity issues and having to go into Unmanaged Code. I do not envy you for having to go that low level language.



来源:https://stackoverflow.com/questions/48272562/how-do-i-properly-import-functions-from-a-c-dll-into-my-c-sharp-project

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!