Calling a Delphi DLL from a C# .NET application

EDIT: I've posted a better implementation of this, below. I left this here so the responses would make sense.

I've done numerous searches for the correct method for writing a DLL in Delphi, and being able to call it from C#, passing and returning strings. A lot of the information was incomplete or incorrect. After much trial and error, I found the solution.

This was compiled using Delphi 2007 and VS 2010. I suspect it will work fine in other versions as well.

Here's the Delphi code. Remember to include version information in the project.

library DelphiLibrary;

uses SysUtils;

// Compiled using Delphi 2007.

// NOTE: If your project doesn't have version information included, you may
// receive the error "The "ResolveManifestFiles" task failed unexpectedly"
// when compiling the C# application.

{$R *.res}

// Example function takes an input integer and input string, and returns
// inputInt + 1, and inputString + ' ' + IntToStr(outputInt) as output
// parameters. If successful, the return result is nil (null), otherwise it is
// the exception message string.


// NOTE: I've posted a better version of this below. You should use that instead.

function DelphiFunction(inputInt : integer; inputString : PAnsiChar;
                        out outputInt : integer; out outputString : PAnsiChar)
                        : PAnsiChar; stdcall; export;
var s : string;
begin
  outputInt := 0;
  outputString := nil;
  try
    outputInt := inputInt + 1;
    s := inputString + ' ' + IntToStr(outputInt);
    outputString := PAnsiChar(s);
    Result := nil;
  except
    on e : exception do Result := PAnsiChar(e.Message);
  end;
end;

// I would have thought having "export" at the end of the function declartion
// (above) would have been enough to export the function, but I couldn't get it
// to work without this line also.
exports DelphiFunction;

begin
end.

Here's the C# code:

using System;
using System.Runtime.InteropServices;

namespace CsharpApp
{
    class Program
    {
        // I added DelphiLibrary.dll to my project (NOT in References, but 
        // "Add existing file"). In Properties for the dll, I set "BuildAction" 
        // to None, and "Copy to Output Directory" to "Copy always".
        // Make sure your Delphi dll has version information included.

        [DllImport("DelphiLibrary.dll", 
                   CallingConvention = CallingConvention.StdCall, 
                   CharSet = CharSet.Ansi)]
        public static extern 
            string DelphiFunction(int inputInt, string inputString,
                                  out int outputInt, out string outputString);

        static void Main(string[] args)
        {
            int inputInt = 1;
            string inputString = "This is a test";
            int outputInt;
            string outputString;


// NOTE: I've posted a better version of this below. You should use that instead.


            Console.WriteLine("inputInt = {0}, intputString = \"{1}\"",
                              inputInt, inputString);
            var errorString = DelphiFunction(inputInt, inputString,
                                             out outputInt, out outputString);
            if (errorString != null)
                Console.WriteLine("Error = \"{0}\"", errorString);
            else
                Console.WriteLine("outputInt = {0}, outputString = \"{1}\"",
                                  outputInt, outputString);
            Console.Write("Press Enter:");
            Console.ReadLine();
        }
    }
}

I hope this information helps someone else to not have to pull their hair out as much as I did.


Based on responses to my post, I have created a new example that uses string buffers for the returned strings, instead of just returning PAnsiChars.

Delphi DLL source:

library DelphiLibrary;

uses SysUtils;

// Compiled using Delphi 2007.

// NOTE: If your project doesn't have version information included, you may
// receive the error "The "ResolveManifestFiles" task failed unexpectedly"
// when compiling the C# application.

{$R *.res}

// A note on returing strings. I had originally written this so that the
// output string was just a PAnsiChar. But several people pointed out that
// since Delphi strings are reference-counted, this was a bad idea since the
// memory for the string could get overwritten before it was used.
//
// Because of this, I re-wrote the example so that you have to pass a buffer for
// the result strings. I saw some examples of how to do this, where they
// returned the actual string length also. This isn't necessary, because the
// string is null-terminated, and in fact the examples themselves never used the
// returned string length.


// Example function takes an input integer and input string, and returns
// inputInt + 1, and inputString + ' ' + IntToStr(outputInt). If successful,
// the return result is true, otherwise errorMsgBuffer contains the the
// exception message string.
function DelphiFunction(inputInt : integer;
                        inputString : PAnsiChar;
                        out outputInt : integer;
                        outputStringBufferSize : integer;
                        var outputStringBuffer : PAnsiChar;
                        errorMsgBufferSize : integer;
                        var errorMsgBuffer : PAnsiChar)
                        : WordBool; stdcall; export;
var s : string;
begin
  outputInt := 0;
  try
    outputInt := inputInt + 1;
    s := inputString + ' ' + IntToStr(outputInt);
    StrLCopy(outputStringBuffer, PAnsiChar(s), outputStringBufferSize-1);
    errorMsgBuffer[0] := #0;
    Result := true;
  except
    on e : exception do
    begin
      StrLCopy(errorMsgBuffer, PAnsiChar(e.Message), errorMsgBufferSize-1);
      Result := false;
    end;
  end;
end;

// I would have thought having "export" at the end of the function declartion
// (above) would have been enough to export the function, but I couldn't get it
// to work without this line also.
exports DelphiFunction;

begin
end.

C# Code:

using System;
using System.Runtime.InteropServices;

namespace CsharpApp
{
    class Program
    {
        // I added DelphiLibrary.dll to my project (NOT in References, but 
        // "Add existing file"). In Properties for the dll, I set "BuildAction" 
        // to None, and "Copy to Output Directory" to "Copy always".
        // Make sure your Delphi dll has version information included.

        [DllImport("DelphiLibrary.dll", 
                   CallingConvention = CallingConvention.StdCall, 
                   CharSet = CharSet.Ansi)]
        public static extern bool 
            DelphiFunction(int inputInt, string inputString,
                           out int outputInt,
                           int outputStringBufferSize, ref string outputStringBuffer,
                           int errorMsgBufferSize, ref string errorMsgBuffer);

        static void Main(string[] args)
        {
            int inputInt = 1;
            string inputString = "This is a test";
            int outputInt;
            const int stringBufferSize = 1024;
            var outputStringBuffer = new String('\x00', stringBufferSize);
            var errorMsgBuffer = new String('\x00', stringBufferSize);

            if (!DelphiFunction(inputInt, inputString, 
                                out outputInt,
                                stringBufferSize, ref outputStringBuffer,
                                stringBufferSize, ref errorMsgBuffer))
                Console.WriteLine("Error = \"{0}\"", errorMsgBuffer);
            else
                Console.WriteLine("outputInt = {0}, outputString = \"{1}\"",
                                  outputInt, outputStringBuffer);

            Console.Write("Press Enter:");
            Console.ReadLine();
        }
    }
}

And here's an additional class that shows how to load the DLL dynamically (sorry for the long lines):

using System;
using System.Runtime.InteropServices;

namespace CsharpApp
{
    static class DynamicLinking
    {
        [DllImport("kernel32.dll", EntryPoint = "LoadLibrary")]
        static extern int LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpLibFileName);

        [DllImport("kernel32.dll", EntryPoint = "GetProcAddress")]
        static extern IntPtr GetProcAddress(int hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName);

        [DllImport("kernel32.dll", EntryPoint = "FreeLibrary")]
        static extern bool FreeLibrary(int hModule);

        [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)]
        delegate bool DelphiFunction(int inputInt, string inputString,
                                     out int outputInt,
                                     int outputStringBufferSize, ref string outputStringBuffer,
                                     int errorMsgBufferSize, ref string errorMsgBuffer);

        public static void CallDelphiFunction(int inputInt, string inputString,
                                              out int outputInt, out string outputString)
        {
            const string dllName = "DelphiLib.dll";
            const string functionName = "DelphiFunction";

            int libHandle = LoadLibrary(dllName);
            if (libHandle == 0)
                throw new Exception(string.Format("Could not load library \"{0}\"", dllName));
            try
            {
                var delphiFunctionAddress = GetProcAddress(libHandle, functionName);
                if (delphiFunctionAddress == IntPtr.Zero)
                    throw new Exception(string.Format("Can't find function \"{0}\" in library \"{1}\"", functionName, dllName));

                var delphiFunction = (DelphiFunction)Marshal.GetDelegateForFunctionPointer(delphiFunctionAddress, typeof(DelphiFunction));

                const int stringBufferSize = 1024;
                var outputStringBuffer = new String('\x00', stringBufferSize);
                var errorMsgBuffer = new String('\x00', stringBufferSize);

                if (!delphiFunction(inputInt, inputString, out outputInt,
                                    stringBufferSize, ref outputStringBuffer,
                                    stringBufferSize, ref errorMsgBuffer))
                    throw new Exception(errorMsgBuffer);

                outputString = outputStringBuffer;
            }
            finally
            {
                FreeLibrary(libHandle);
            }
        }
    }
}

-Dan


As Jeroen Pluimers said in his comment, you should take note that Delphi strings are reference-counted.

IMO, in such circumstances which you are supposed to return a string in heterogeneous environments, you should ask the caller to provide a buffer for the result, and the function should fill that buffer. This way, the caller is responsible for creating the buffer and disposing it when it is done with it. If you take a look at Win32 API functions, you'll see they do the same when they need to return a string to a caller.

To do so, you can use PChar (either PAnsiChar or PWideChar) as the type of function parameter, but you should also ask caller to provide size of the buffer too. Take a look at my answer in the link below, for a sample source code:

Exchanging strings (PChar) between a Freepascal compiled DLL and a Delphi compiled EXE

The question is specifically about exchanging string between FreePascal and Delphi, but the idea and the answer is applicable to your case too.