D. Pittman
D. Pittman

Reputation: 13

Converting Delphi non-COM DLL Interface to C#

I/we currently have a program written in Delphi (XE, specifically) which interfaces with an unmanaged DLL (written in C++, from what I hear). The Delphi application is being retired, but the need to work with the unmanaged DLL is sticking around, so a C# interface needs to be written. No big deal. Except not having access to the DLL's source or any good documentation on it, as well as this being my first foray into interop, is killing me.

Here's the documentation on the functions available in the DLL:

  extern EXTERNC __declspec( dllexport ) long initiate (
    double conc1,               
    double conc2,                   
    long temp,
    const char* NT
  );

  extern EXTERNC __declspec( dllexport ) long DoWork (
    long    NumItems,
    struct  Structure* Items
  );

In Delphi, these are successfully implemented like this (with the custom structure also included):

  function CustomInitialize
    (
    Concentration1 : double;
    Concentration2 : double;
    Temperature : LongInt;
    const Type : PAnsiChar
    ) : LongInt; cdecl; external 'CustomDLL.dll' name '_initiate';

  procedure DoWork
    (
    NumItems : LongInt;
    Items : PStructure
    ); cdecl; external 'CustomDLL.dll' name '_DoWork';

  TStructure = record
    ValueName : PAnsiChar;
    Value : PAnsiChar;
    Status : LongInt;       
    ReturnVal1 : Double;
    ReturnVal2 : Double;
    ReturnVal3 : Double;
    Temp : Double;
  end;
  PStructure = ^TStructure;

Note that while the DoWork method appears to take in an array of items, all implementations set NumItems to 1 and loop over the object in Delphi, rather than passing this off to C++.

In C#, I'm not even sure which example I should post. I've googled for days and tried what feels like every version of the code that I can try, but all to no avail. Here's the most recent version:

namespace JunkProject
{
    class Program
    {
        static void Main(string[] args)
        {
            int test = _initiate(.05, .05, 60, "1");

            Console.WriteLine(test);
            if (test != 1)
                Console.ReadLine();

            var structure = new FoldStructure() { ValueName = "Test1", Value = "TESTTESTTESTTESTTESTTESTTEST", Status = 0, ReturnVal1 = 0.0, ReturnVal2 = 0.0, ReturnVal3 = 0.0, Temp = 0.0 };

            test = _DoWork(1, structure);

            Console.WriteLine(structure.Value);
            Console.ReadLine();
        }

        private const string DLL_LOCATION = "CustomDLL.dll";

        [DllImport(DLL_LOCATION, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        private static extern int _initiate(double Conc1, double Conc2, int Temp, [MarshalAs(UnmanagedType.LPStr, SizeConst = 5)] string Type);

        [DllImport(DLL_LOCATION, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        private static extern int _DoWork(int NumItems, [In, Out] Structure Struct);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        public class Structure
        {
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 5)]
            public string ValueName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 28)]
            public string Value;

            public int Status;
            public double ReturnVal1;
            public double ReturnVal2;
            public double ReturnVal3;
            public double Temp;
        }
    }
}

Doing this, however, provides an access violation. I have tried making the method signature an IntPtr, this fails. I have tried making the method signature a pointer to the struct, this usually goes horribly wrong in every way that I try it, though I can't be sure that I've really hammered away at 'the right way' for very long, since I'm not sure what that way is. I think if I could figure out what the correct method signature is, that would help a ton.

Also, apologies for slightly obfuscating the source. The dll I'm using is proprietary and whatnot.

Preemptive thanks for any help!

Upvotes: 1

Views: 608

Answers (1)

Remy Lebeau
Remy Lebeau

Reputation: 595329

You should not be using the SizeConst attribute in your MarshalAs declaration of the Type parameter in _initiate(). The input is just a pointer to a character string, so let C# marshal it as such.

_DoWork() expects an array (even if your implementations only ever pass 1 element), so you should marshal an actual array.

You should be using a struct instead of a class for the Structure type. And the declaration of the ValueName and Value fields does does not match your Delphi code. In your Delphi code, they are just raw pointers, presumably to allocated character buffers. But in your C# code, you are marshalling variable-length string values as if they were fixed-length character arrays.

Try something more like this instead:

namespace JunkProject
{
    class Program
    {
        static void Main(string[] args)
        {
            int test = initiate(.05, .05, 60, "1");

            Console.WriteLine(test);
            if (test != 1)
                Console.ReadLine();

            Structure[] structure = new Structure[1];
            structure[0].ValueName = "Test1";
            structure[0].Value = "TESTTESTTESTTESTTESTTESTTEST";
            structure[0].Status = 0;
            structure[0].ReturnVal1 = 0.0;
            structure[0].ReturnVal2 = 0.0;
            structure[0].ReturnVal3 = 0.0;
            structure[0].Temp = 0.0;

            test = DoWork(1, structure);

            Console.WriteLine(structure[0].Value);
            Console.ReadLine();
        }

        private const string DLL_LOCATION = "CustomDLL.dll";

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        public struct Structure
        {
            [MarshalAs(UnmanagedType.LPStr)]
            public string ValueName;
            [MarshalAs(UnmanagedType.LPStr)]
            public string Value;
            public int Status;
            public double ReturnVal1;
            public double ReturnVal2;
            public double ReturnVal3;
            public double Temp;
        }

        [DllImport(DLL_LOCATION, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, EntryPoint = "_initiate")]
        private static extern int initiate(double Conc1, double Conc2, int Temp, [MarshalAs(UnmanagedType.LPStr)] string Type);

        [DllImport(DLL_LOCATION, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, EntryPoint = "_DoWork")]
        private static extern int DoWork(int NumItems, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] Structure[] Struct);
    }
}

However, you are reading the Structure.Value field after _DoWork() exits, so presumably it will be writing new data into that field, so you may need to do something more like this instead:

namespace JunkProject
{
    class Program
    {
        static void Main(string[] args)
        {
            int test = initiate(.05, .05, 60, "1");

            Console.WriteLine(test);
            if (test != 1)
                Console.ReadLine();

            Structure[] structure = new Structure[1];

            structure[0].ValueName = "Test1";
            structure[0].Value = Marshal.AllocHGlobal(28);
            // alternatively:
            // structure[0].Value = (IntPtr) Marshal.StringToHGlobalAnsi("TESTTESTTESTTESTTESTTESTTEST");
            structure[0].Status = 0;
            structure[0].ReturnVal1 = 0.0;
            structure[0].ReturnVal2 = 0.0;
            structure[0].ReturnVal3 = 0.0;
            structure[0].Temp = 0.0;

            test = DoWork(1, structure);

            String Value = Marshal.PtrToStringAnsi(structure[0].Value);
            Console.WriteLine(Value);
            Console.ReadLine();

            Marshal.FreeHGlobal(structure[0].Value);
        }

        private const string DLL_LOCATION = "CustomDLL.dll";

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        public struct Structure
        {
            [MarshalAs(UnmanagedType.LPStr)]
            public string ValueName;
            public IntPtr Value;
            public int Status;
            public double ReturnVal1;
            public double ReturnVal2;
            public double ReturnVal3;
            public double Temp;
        }

        [DllImport(DLL_LOCATION, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, EntryPoint = "_initiate")]
        private static extern int initiate(double Conc1, double Conc2, int Temp, [MarshalAs(UnmanagedType.LPStr)] string Type);

        [DllImport(DLL_LOCATION, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, EntryPoint = "_DoWork")]
        private static extern int DoWork(int NumItems, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] Structure[] Struct);
    }
}

Upvotes: 1

Related Questions